refactor: deduplicate setup wizard helpers
This commit is contained in:
parent
1c81b82f48
commit
1a9114a169
@ -1,11 +1,10 @@
|
||||
import {
|
||||
createAllowFromSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
resolveSetupAccountId,
|
||||
promptParsedAllowFromForAccount,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
@ -55,14 +54,13 @@ async function promptBlueBubblesAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultBlueBubblesAccountId(params.cfg),
|
||||
});
|
||||
const resolved = resolveBlueBubblesAccount({ cfg: params.cfg, accountId });
|
||||
const existing = resolved.config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
prompter: params.prompter,
|
||||
noteTitle: "BlueBubbles allowlist",
|
||||
noteLines: [
|
||||
"Allowlist BlueBubbles DMs by handle or chat target.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
@ -71,30 +69,23 @@ async function promptBlueBubblesAllowFrom(params: {
|
||||
"- chat_guid:iMessage;-;+15555550123",
|
||||
"Multiple entries: comma- or newline-separated.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
].join("\n"),
|
||||
"BlueBubbles allowlist",
|
||||
);
|
||||
const entry = await params.prompter.text({
|
||||
],
|
||||
message: "BlueBubbles allowFrom (handle or chat_id)",
|
||||
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = parseBlueBubblesAllowFromInput(raw);
|
||||
for (const part of parts) {
|
||||
if (!validateBlueBubblesAllowFromEntry(part)) {
|
||||
return `Invalid entry: ${part}`;
|
||||
parseEntries: (raw) => {
|
||||
const entries = parseBlueBubblesAllowFromInput(raw);
|
||||
for (const entry of entries) {
|
||||
if (!validateBlueBubblesAllowFromEntry(entry)) {
|
||||
return { entries: [], error: `Invalid entry: ${entry}` };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return { entries };
|
||||
},
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveBlueBubblesAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
setBlueBubblesAllowFrom(cfg, accountId, allowFrom),
|
||||
});
|
||||
const parts = parseBlueBubblesAllowFromInput(String(entry));
|
||||
const unique = mergeAllowFromEntries(undefined, parts);
|
||||
return setBlueBubblesAllowFrom(params.cfg, accountId, unique);
|
||||
}
|
||||
|
||||
function validateBlueBubblesServerUrlInput(value: unknown): string | undefined {
|
||||
@ -272,7 +263,7 @@ export const blueBubblesSetupWizard: ChannelSetupWizard = {
|
||||
],
|
||||
},
|
||||
dmPolicy,
|
||||
allowFrom: {
|
||||
allowFrom: createAllowFromSection({
|
||||
helpTitle: "BlueBubbles allowlist",
|
||||
helpLines: [
|
||||
"Allowlist BlueBubbles DMs by handle or chat target.",
|
||||
@ -290,15 +281,9 @@ export const blueBubblesSetupWizard: ChannelSetupWizard = {
|
||||
"Use a BlueBubbles handle or chat target like +15555550123 or chat_id:123.",
|
||||
parseInputs: parseBlueBubblesAllowFromInput,
|
||||
parseId: (raw) => validateBlueBubblesAllowFromEntry(raw),
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => ({
|
||||
input: entry,
|
||||
resolved: Boolean(validateBlueBubblesAllowFromEntry(entry)),
|
||||
id: validateBlueBubblesAllowFromEntry(entry),
|
||||
})),
|
||||
apply: async ({ cfg, accountId, allowFrom }) =>
|
||||
setBlueBubblesAllowFrom(cfg, accountId, allowFrom),
|
||||
},
|
||||
}),
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
|
||||
@ -191,11 +191,9 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
export function createDiscordSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ discordSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
export function createDiscordSetupWizardProxy(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return createAllowlistSetupWizardProxy({
|
||||
loadWizard: async () => (await loadWizard()).discordSetupWizard,
|
||||
loadWizard,
|
||||
createBase: createDiscordSetupWizardBase,
|
||||
fallbackResolvedGroupAllowlist: (entries) =>
|
||||
entries.map((input) => ({ input, resolved: false })),
|
||||
|
||||
@ -23,9 +23,9 @@ async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
|
||||
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
}));
|
||||
export const discordSetupWizard = createDiscordSetupWizardProxy(
|
||||
async () => (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
);
|
||||
|
||||
export const discordConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptParsedAllowFromForAccount,
|
||||
promptSingleChannelSecretInput,
|
||||
splitSetupEntries,
|
||||
type ChannelSetupDmPolicy,
|
||||
@ -96,34 +97,25 @@ async function promptFeishuAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
|
||||
}): Promise<OpenClawConfig> {
|
||||
const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
prompter: params.prompter,
|
||||
noteTitle: "Feishu allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Feishu DMs by open_id or user_id.",
|
||||
"You can find user open_id in Feishu admin console or via API.",
|
||||
"Examples:",
|
||||
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
].join("\n"),
|
||||
"Feishu allowlist",
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const entry = await params.prompter.text({
|
||||
message: "Feishu allowFrom (user open_ids)",
|
||||
placeholder: "ou_xxxxx, ou_yyyyy",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = splitSetupEntries(String(entry));
|
||||
if (parts.length === 0) {
|
||||
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
||||
continue;
|
||||
}
|
||||
|
||||
const unique = mergeAllowFromEntries(existing, parts);
|
||||
return setFeishuAllowFrom(params.cfg, unique);
|
||||
}
|
||||
],
|
||||
message: "Feishu allowFrom (user open_ids)",
|
||||
placeholder: "ou_xxxxx, ou_yyyyy",
|
||||
parseEntries: (raw) => ({ entries: splitSetupEntries(raw) }),
|
||||
getExistingAllowFrom: ({ cfg }) => cfg.channels?.feishu?.allowFrom ?? [],
|
||||
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing, parsed),
|
||||
applyAllowFrom: ({ cfg, allowFrom }) => setFeishuAllowFrom(cfg, allowFrom),
|
||||
});
|
||||
}
|
||||
|
||||
async function noteFeishuCredentialHelp(
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import {
|
||||
createScopedDmSecurityResolver,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { buildOutboundBaseSessionKey } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
@ -23,18 +19,16 @@ import {
|
||||
} from "./group-policy.js";
|
||||
import { getIMessageRuntime } from "./runtime.js";
|
||||
import { imessageSetupAdapter } from "./setup-core.js";
|
||||
import { createIMessagePluginBase, imessageSetupWizard } from "./shared.js";
|
||||
import {
|
||||
collectIMessageSecurityWarnings,
|
||||
createIMessagePluginBase,
|
||||
imessageResolveDmPolicy,
|
||||
imessageSetupWizard,
|
||||
} from "./shared.js";
|
||||
import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";
|
||||
|
||||
const loadIMessageChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
|
||||
|
||||
const resolveIMessageDmPolicy = createScopedDmSecurityResolver<ResolvedIMessageAccount>({
|
||||
channelKey: "imessage",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
});
|
||||
|
||||
function buildIMessageBaseSessionKey(params: {
|
||||
cfg: Parameters<typeof resolveIMessageAccount>[0]["cfg"];
|
||||
agentId: string;
|
||||
@ -136,19 +130,8 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: resolveIMessageDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: imessageResolveDmPolicy,
|
||||
collectWarnings: collectIMessageSecurityWarnings,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import {
|
||||
createCliPathTextInput,
|
||||
createDelegatedSetupWizardProxy,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
createPatchedAccountSetupAdapter,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptParsedAllowFromForAccount,
|
||||
setAccountAllowFromForChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
@ -71,9 +75,8 @@ export async function promptIMessageAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return promptParsedAllowFromForScopedChannel({
|
||||
return promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultIMessageAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
@ -93,6 +96,13 @@ export async function promptIMessageAllowFrom(params: {
|
||||
parseEntries: parseIMessageAllowFromEntries,
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
setAccountAllowFromForChannel({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@ -118,17 +128,14 @@ function resolveIMessageCliPath(params: { cfg: OpenClawConfig; accountId: string
|
||||
export function createIMessageCliPathTextInput(
|
||||
shouldPrompt: NonNullable<ChannelSetupWizardTextInput["shouldPrompt"]>,
|
||||
): ChannelSetupWizardTextInput {
|
||||
return {
|
||||
return createCliPathTextInput({
|
||||
inputKey: "cliPath",
|
||||
message: "imsg CLI path",
|
||||
initialValue: ({ cfg, accountId }) => resolveIMessageCliPath({ cfg, accountId }),
|
||||
currentValue: ({ cfg, accountId }) => resolveIMessageCliPath({ cfg, accountId }),
|
||||
resolvePath: ({ cfg, accountId }) => resolveIMessageCliPath({ cfg, accountId }),
|
||||
shouldPrompt,
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "iMessage",
|
||||
helpLines: ["imsg CLI path required to enable iMessage."],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const imessageCompletionNote = {
|
||||
@ -167,31 +174,29 @@ export const imessageSetupStatusBase = {
|
||||
}),
|
||||
};
|
||||
|
||||
export function createIMessageSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
return {
|
||||
export function createIMessageSetupWizardProxy(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return createDelegatedSetupWizardProxy({
|
||||
channel,
|
||||
loadWizard,
|
||||
status: {
|
||||
...imessageSetupStatusBase,
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
configuredLabel: imessageSetupStatusBase.configuredLabel,
|
||||
unconfiguredLabel: imessageSetupStatusBase.unconfiguredLabel,
|
||||
configuredHint: imessageSetupStatusBase.configuredHint,
|
||||
unconfiguredHint: imessageSetupStatusBase.unconfiguredHint,
|
||||
configuredScore: imessageSetupStatusBase.configuredScore,
|
||||
unconfiguredScore: imessageSetupStatusBase.unconfiguredScore,
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
createIMessageCliPathTextInput(async (params) => {
|
||||
const input = (await loadWizard()).imessageSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
}),
|
||||
createIMessageCliPathTextInput(
|
||||
createDelegatedTextInputShouldPrompt({
|
||||
loadWizard,
|
||||
inputKey: "cliPath",
|
||||
}),
|
||||
),
|
||||
],
|
||||
completionNote: imessageCompletionNote,
|
||||
dmPolicy: imessageDmPolicy,
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { setSetupChannelEnabled, type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createDetectedBinaryStatus,
|
||||
setSetupChannelEnabled,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { detectBinary } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { listIMessageAccountIds, resolveIMessageAccount } from "./accounts.js";
|
||||
import {
|
||||
@ -14,25 +18,19 @@ const channel = "imessage" as const;
|
||||
|
||||
export const imessageSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
...imessageSetupStatusBase,
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
const cliDetected = await detectBinary(cliPath);
|
||||
return [
|
||||
`iMessage: ${configured ? "configured" : "needs setup"}`,
|
||||
`imsg: ${cliDetected ? "found" : "missing"} (${cliPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? "imsg found" : "imsg missing";
|
||||
},
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? 1 : 0;
|
||||
},
|
||||
},
|
||||
status: createDetectedBinaryStatus({
|
||||
channelLabel: "iMessage",
|
||||
binaryLabel: "imsg",
|
||||
configuredLabel: imessageSetupStatusBase.configuredLabel,
|
||||
unconfiguredLabel: imessageSetupStatusBase.unconfiguredLabel,
|
||||
configuredHint: imessageSetupStatusBase.configuredHint,
|
||||
unconfiguredHint: imessageSetupStatusBase.unconfiguredHint,
|
||||
configuredScore: imessageSetupStatusBase.configuredScore,
|
||||
unconfiguredScore: imessageSetupStatusBase.unconfiguredScore,
|
||||
resolveConfigured: imessageSetupStatusBase.resolveConfigured,
|
||||
resolveBinaryPath: ({ cfg }) => cfg.channels?.imessage?.cliPath ?? "imsg",
|
||||
detectBinary,
|
||||
}),
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
createIMessageCliPathTextInput(async ({ currentValue }) => {
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatTrimmedAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
IMessageConfigSchema,
|
||||
resolveIMessageConfigAllowFrom,
|
||||
resolveIMessageConfigDefaultTo,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/imessage-core";
|
||||
import {
|
||||
@ -29,9 +25,47 @@ async function loadIMessageChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
}));
|
||||
export const imessageSetupWizard = createIMessageSetupWizardProxy(
|
||||
async () => (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
);
|
||||
|
||||
export const imessageConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveIMessageAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedIMessageAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
|
||||
resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const imessageConfigBase = createScopedChannelConfigBase<ResolvedIMessageAccount>({
|
||||
sectionKey: IMESSAGE_CHANNEL,
|
||||
listAccountIds: listIMessageAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultIMessageAccountId,
|
||||
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
|
||||
});
|
||||
|
||||
export const imessageResolveDmPolicy = createScopedDmSecurityResolver<ResolvedIMessageAccount>({
|
||||
channelKey: IMESSAGE_CHANNEL,
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
});
|
||||
|
||||
export function collectIMessageSecurityWarnings(params: {
|
||||
account: ResolvedIMessageAccount;
|
||||
cfg: Parameters<typeof resolveIMessageAccount>[0]["cfg"];
|
||||
}) {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg: params.cfg,
|
||||
providerConfigPresent: params.cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: params.account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function createIMessagePluginBase(params: {
|
||||
setupWizard?: NonNullable<ChannelPlugin<ResolvedIMessageAccount>["setupWizard"]>;
|
||||
@ -63,24 +97,7 @@ export function createIMessagePluginBase(params: {
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
|
||||
}),
|
||||
...imessageConfigBase,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
@ -88,32 +105,11 @@ export function createIMessagePluginBase(params: {
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
|
||||
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
|
||||
...imessageConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
resolveDmPolicy: imessageResolveDmPolicy,
|
||||
collectWarnings: collectIMessageSecurityWarnings,
|
||||
},
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveSetupAccountId, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createAllowFromSection,
|
||||
promptParsedAllowFromForAccount,
|
||||
setSetupChannelEnabled,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
|
||||
@ -53,36 +57,30 @@ async function promptIrcAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const existing = params.cfg.channels?.irc?.allowFrom ?? [];
|
||||
|
||||
await params.prompter.note(
|
||||
[
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultIrcAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
noteTitle: "IRC allowlist",
|
||||
noteLines: [
|
||||
"Allowlist IRC DMs by sender.",
|
||||
"Examples:",
|
||||
"- alice",
|
||||
"- alice!ident@example.org",
|
||||
"Multiple entries: comma-separated.",
|
||||
].join("\n"),
|
||||
"IRC allowlist",
|
||||
);
|
||||
|
||||
const raw = await params.prompter.text({
|
||||
],
|
||||
message: "IRC allowFrom (nick or nick!user@host)",
|
||||
placeholder: "alice, bob!ident@example.org",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
|
||||
const parsed = parseListInput(String(raw));
|
||||
const normalized = [
|
||||
...new Set(
|
||||
parsed
|
||||
parseEntries: (raw) => ({
|
||||
entries: parseListInput(raw)
|
||||
.map((entry) => normalizeIrcAllowEntry(entry))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
return setIrcAllowFrom(params.cfg, normalized);
|
||||
}),
|
||||
getExistingAllowFrom: ({ cfg }) => cfg.channels?.irc?.allowFrom ?? [],
|
||||
applyAllowFrom: ({ cfg, allowFrom }) => setIrcAllowFrom(cfg, allowFrom),
|
||||
});
|
||||
}
|
||||
|
||||
async function promptIrcNickServConfig(params: {
|
||||
@ -173,10 +171,7 @@ const ircDmPolicy: ChannelSetupDmPolicy = {
|
||||
await promptIrcAllowFrom({
|
||||
cfg: cfg as CoreConfig,
|
||||
prompter,
|
||||
accountId: resolveSetupAccountId({
|
||||
accountId,
|
||||
defaultAccountId: resolveDefaultIrcAccountId(cfg as CoreConfig),
|
||||
}),
|
||||
accountId,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -388,7 +383,7 @@ export const ircSetupWizard: ChannelSetupWizard = {
|
||||
normalizeGroupEntry,
|
||||
),
|
||||
},
|
||||
allowFrom: {
|
||||
allowFrom: createAllowFromSection({
|
||||
helpTitle: "IRC allowlist",
|
||||
helpLines: [
|
||||
"Allowlist IRC DMs by sender.",
|
||||
@ -404,17 +399,8 @@ export const ircSetupWizard: ChannelSetupWizard = {
|
||||
const normalized = normalizeIrcAllowEntry(raw);
|
||||
return normalized || null;
|
||||
},
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => {
|
||||
const normalized = normalizeIrcAllowEntry(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(normalized),
|
||||
id: normalized || null,
|
||||
};
|
||||
}),
|
||||
apply: async ({ cfg, allowFrom }) => setIrcAllowFrom(cfg as CoreConfig, allowFrom),
|
||||
},
|
||||
}),
|
||||
finalize: async ({ cfg, accountId, prompter }) => {
|
||||
let next = cfg as CoreConfig;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
createTopLevelChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
resolveLineAccount,
|
||||
@ -8,6 +7,7 @@ import {
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/line-core";
|
||||
import { createAllowFromSection, createTopLevelChannelDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
isLineConfigured,
|
||||
listLineAccountIds,
|
||||
@ -156,7 +156,7 @@ export const lineSetupWizard: ChannelSetupWizard = {
|
||||
}),
|
||||
},
|
||||
],
|
||||
allowFrom: {
|
||||
allowFrom: createAllowFromSection({
|
||||
helpTitle: "LINE allowlist",
|
||||
helpLines: LINE_ALLOW_FROM_HELP_LINES,
|
||||
message: "LINE allowFrom (user id)",
|
||||
@ -165,15 +165,6 @@ export const lineSetupWizard: ChannelSetupWizard = {
|
||||
"LINE allowFrom requires raw user ids like U1234567890abcdef1234567890abcdef.",
|
||||
parseInputs: splitSetupEntries,
|
||||
parseId: parseLineAllowFromId,
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => {
|
||||
const id = parseLineAllowFromId(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(id),
|
||||
id,
|
||||
};
|
||||
}),
|
||||
apply: ({ cfg, accountId, allowFrom }) =>
|
||||
patchLineAccountConfig({
|
||||
cfg,
|
||||
@ -181,7 +172,7 @@ export const lineSetupWizard: ChannelSetupWizard = {
|
||||
enabled: true,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
dmPolicy: lineDmPolicy,
|
||||
completionNote: {
|
||||
title: "LINE webhook",
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
createTopLevelChannelDmPolicy,
|
||||
promptParsedAllowFromForAccount,
|
||||
resolveSetupAccountId,
|
||||
setSetupChannelEnabled,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
@ -112,41 +113,38 @@ async function promptNextcloudTalkAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const resolved = resolveNextcloudTalkAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: params.accountId,
|
||||
prompter: params.prompter,
|
||||
noteTitle: "Nextcloud Talk user id",
|
||||
noteLines: [
|
||||
"1) Check the Nextcloud admin panel for user IDs",
|
||||
"2) Or look at the webhook payload logs when someone messages",
|
||||
"3) User IDs are typically lowercase usernames in Nextcloud",
|
||||
`Docs: ${formatDocsLink("/channels/nextcloud-talk", "nextcloud-talk")}`,
|
||||
].join("\n"),
|
||||
"Nextcloud Talk user id",
|
||||
);
|
||||
|
||||
let resolvedIds: string[] = [];
|
||||
while (resolvedIds.length === 0) {
|
||||
const entry = await params.prompter.text({
|
||||
message: "Nextcloud Talk allowFrom (user id)",
|
||||
placeholder: "username",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
resolvedIds = String(entry)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (resolvedIds.length === 0) {
|
||||
await params.prompter.note("Please enter at least one valid user ID.", "Nextcloud Talk");
|
||||
}
|
||||
}
|
||||
|
||||
return setNextcloudTalkAccountConfig(params.cfg, params.accountId, {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(
|
||||
existingAllowFrom.map((value) => String(value).trim().toLowerCase()),
|
||||
resolvedIds,
|
||||
),
|
||||
],
|
||||
message: "Nextcloud Talk allowFrom (user id)",
|
||||
placeholder: "username",
|
||||
parseEntries: (raw) => ({
|
||||
entries: String(raw)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveNextcloudTalkAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
mergeEntries: ({ existing, parsed }) =>
|
||||
mergeAllowFromEntries(
|
||||
existing.map((value) => String(value).trim().toLowerCase()),
|
||||
parsed,
|
||||
),
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
setNextcloudTalkAccountConfig(cfg, accountId, {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
mergeAllowFromEntries,
|
||||
parseSetupEntriesWithParser,
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptParsedAllowFromForAccount,
|
||||
splitSetupEntries,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
@ -71,22 +72,19 @@ async function promptNostrAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const existing = params.cfg.channels?.nostr?.allowFrom ?? [];
|
||||
await params.prompter.note(NOSTR_ALLOW_FROM_HELP_LINES.join("\n"), "Nostr allowlist");
|
||||
const entry = await params.prompter.text({
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
prompter: params.prompter,
|
||||
noteTitle: "Nostr allowlist",
|
||||
noteLines: NOSTR_ALLOW_FROM_HELP_LINES,
|
||||
message: "Nostr allowFrom",
|
||||
placeholder: "npub1..., 0123abcd...",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
return parseNostrAllowFrom(raw).error;
|
||||
},
|
||||
parseEntries: parseNostrAllowFrom,
|
||||
getExistingAllowFrom: ({ cfg }) => cfg.channels?.nostr?.allowFrom ?? [],
|
||||
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing, parsed),
|
||||
applyAllowFrom: ({ cfg, allowFrom }) => setNostrAllowFrom(cfg, allowFrom),
|
||||
});
|
||||
const parsed = parseNostrAllowFrom(String(entry));
|
||||
return setNostrAllowFrom(params.cfg, mergeAllowFromEntries(existing, parsed.entries));
|
||||
}
|
||||
|
||||
const nostrDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/allowlist-config-edit";
|
||||
import {
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildOutboundBaseSessionKey } from "openclaw/plugin-sdk/core";
|
||||
@ -33,15 +29,13 @@ import { signalMessageActions } from "./message-actions.js";
|
||||
import type { SignalProbe } from "./probe.js";
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
import { signalSetupAdapter } from "./setup-core.js";
|
||||
import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js";
|
||||
|
||||
const resolveSignalDmPolicy = createScopedDmSecurityResolver<ResolvedSignalAccount>({
|
||||
channelKey: "signal",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
});
|
||||
import {
|
||||
collectSignalSecurityWarnings,
|
||||
createSignalPluginBase,
|
||||
signalConfigAccessors,
|
||||
signalResolveDmPolicy,
|
||||
signalSetupWizard,
|
||||
} from "./shared.js";
|
||||
type SignalSendFn = ReturnType<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
|
||||
|
||||
function resolveSignalSendContext(params: {
|
||||
@ -304,19 +298,8 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: resolveSignalDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
},
|
||||
resolveDmPolicy: signalResolveDmPolicy,
|
||||
collectWarnings: collectSignalSecurityWarnings,
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import {
|
||||
createCliPathTextInput,
|
||||
createDelegatedSetupWizardProxy,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
createPatchedAccountSetupAdapter,
|
||||
normalizeE164,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptParsedAllowFromForAccount,
|
||||
setAccountAllowFromForChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
@ -89,9 +93,8 @@ export async function promptSignalAllowFrom(params: {
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return promptParsedAllowFromForScopedChannel({
|
||||
return promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSignalAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
@ -109,6 +112,13 @@ export async function promptSignalAllowFrom(params: {
|
||||
parseEntries: parseSignalAllowFromEntries,
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
setAccountAllowFromForChannel({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,21 +154,17 @@ function resolveSignalCliPath(params: {
|
||||
export function createSignalCliPathTextInput(
|
||||
shouldPrompt: NonNullable<ChannelSetupWizardTextInput["shouldPrompt"]>,
|
||||
): ChannelSetupWizardTextInput {
|
||||
return {
|
||||
return createCliPathTextInput({
|
||||
inputKey: "cliPath",
|
||||
message: "signal-cli path",
|
||||
currentValue: ({ cfg, accountId, credentialValues }) =>
|
||||
resolveSignalCliPath({ cfg, accountId, credentialValues }),
|
||||
initialValue: ({ cfg, accountId, credentialValues }) =>
|
||||
resolvePath: ({ cfg, accountId, credentialValues }) =>
|
||||
resolveSignalCliPath({ cfg, accountId, credentialValues }),
|
||||
shouldPrompt,
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "Signal",
|
||||
helpLines: [
|
||||
"signal-cli not found. Install it, then rerun this step or set channels.signal.cliPath.",
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const signalNumberTextInput: ChannelSetupWizardTextInput = {
|
||||
@ -200,11 +206,10 @@ export const signalSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetup
|
||||
buildPatch: (input) => buildSignalSetupPatch(input),
|
||||
});
|
||||
|
||||
export function createSignalSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ signalSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
return {
|
||||
export function createSignalSetupWizardProxy(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return createDelegatedSetupWizardProxy({
|
||||
channel,
|
||||
loadWizard,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
@ -212,30 +217,20 @@ export function createSignalSetupWizardProxy(
|
||||
unconfiguredHint: "signal-cli missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).signalSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
},
|
||||
prepare: async (params) => await (await loadWizard()).signalSetupWizard.prepare?.(params),
|
||||
delegatePrepare: true,
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
createSignalCliPathTextInput(async (params) => {
|
||||
const input = (await loadWizard()).signalSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
}),
|
||||
createSignalCliPathTextInput(
|
||||
createDelegatedTextInputShouldPrompt({
|
||||
loadWizard,
|
||||
inputKey: "cliPath",
|
||||
}),
|
||||
),
|
||||
signalNumberTextInput,
|
||||
],
|
||||
completionNote: signalCompletionNote,
|
||||
dmPolicy: signalDmPolicy,
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { setSetupChannelEnabled, type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createDetectedBinaryStatus,
|
||||
setSetupChannelEnabled,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { detectBinary, installSignalCli } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { listSignalAccountIds, resolveSignalAccount } from "./accounts.js";
|
||||
import {
|
||||
@ -15,7 +19,9 @@ const channel = "signal" as const;
|
||||
|
||||
export const signalSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
status: createDetectedBinaryStatus({
|
||||
channelLabel: "Signal",
|
||||
binaryLabel: "signal-cli",
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "signal-cli found",
|
||||
@ -26,23 +32,9 @@ export const signalSetupWizard: ChannelSetupWizard = {
|
||||
listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
const signalCliDetected = await detectBinary(signalCliPath);
|
||||
return [
|
||||
`Signal: ${configured ? "configured" : "needs setup"}`,
|
||||
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? "signal-cli found" : "signal-cli missing";
|
||||
},
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? 1 : 0;
|
||||
},
|
||||
},
|
||||
resolveBinaryPath: ({ cfg }) => cfg.channels?.signal?.cliPath ?? "signal-cli",
|
||||
detectBinary,
|
||||
}),
|
||||
prepare: async ({ cfg, accountId, credentialValues, runtime, prompter, options }) => {
|
||||
if (!options?.allowSignalInstall) {
|
||||
return;
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { createScopedAccountConfigAccessors } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
setAccountEnabledInConfigSection,
|
||||
SignalConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/signal-core";
|
||||
@ -28,9 +26,9 @@ async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
export const signalSetupWizard = createSignalSetupWizardProxy(
|
||||
async () => (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
);
|
||||
|
||||
export const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
|
||||
@ -44,6 +42,38 @@ export const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const signalConfigBase = createScopedChannelConfigBase<ResolvedSignalAccount>({
|
||||
sectionKey: SIGNAL_CHANNEL,
|
||||
listAccountIds: listSignalAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultSignalAccountId,
|
||||
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
|
||||
});
|
||||
|
||||
export const signalResolveDmPolicy = createScopedDmSecurityResolver<ResolvedSignalAccount>({
|
||||
channelKey: SIGNAL_CHANNEL,
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
});
|
||||
|
||||
export function collectSignalSecurityWarnings(params: {
|
||||
account: ResolvedSignalAccount;
|
||||
cfg: Parameters<typeof resolveSignalAccount>[0]["cfg"];
|
||||
}) {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg: params.cfg,
|
||||
providerConfigPresent: params.cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: params.account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function createSignalPluginBase(params: {
|
||||
setupWizard?: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setupWizard"]>;
|
||||
setup: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setup"]>;
|
||||
@ -77,24 +107,7 @@ export function createSignalPluginBase(params: {
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSignalAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
|
||||
}),
|
||||
...signalConfigBase,
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
@ -106,28 +119,8 @@ export function createSignalPluginBase(params: {
|
||||
...signalConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
resolveDmPolicy: signalResolveDmPolicy,
|
||||
collectWarnings: collectSignalSecurityWarnings,
|
||||
},
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
createAllowFromSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
@ -281,7 +282,7 @@ export const synologyChatSetupWizard: ChannelSetupWizard = {
|
||||
}),
|
||||
},
|
||||
],
|
||||
allowFrom: {
|
||||
allowFrom: createAllowFromSection({
|
||||
helpTitle: "Synology Chat allowlist",
|
||||
helpLines: SYNOLOGY_ALLOW_FROM_HELP_LINES,
|
||||
message: "Allowed Synology Chat user ids",
|
||||
@ -289,15 +290,6 @@ export const synologyChatSetupWizard: ChannelSetupWizard = {
|
||||
invalidWithoutCredentialNote: "Synology Chat user ids must be numeric.",
|
||||
parseInputs: splitSetupEntries,
|
||||
parseId: parseSynologyUserId,
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => {
|
||||
const id = parseSynologyUserId(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(id),
|
||||
id,
|
||||
};
|
||||
}),
|
||||
apply: async ({ cfg, accountId, allowFrom }) =>
|
||||
patchSynologyChatAccountConfig({
|
||||
cfg,
|
||||
@ -311,7 +303,7 @@ export const synologyChatSetupWizard: ChannelSetupWizard = {
|
||||
),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
completionNote: {
|
||||
title: "Synology Chat access control",
|
||||
lines: [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
createAllowFromSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
hasConfiguredSecretInput,
|
||||
type OpenClawConfig,
|
||||
@ -80,7 +81,7 @@ export const telegramSetupWizard: ChannelSetupWizard = {
|
||||
},
|
||||
},
|
||||
],
|
||||
allowFrom: {
|
||||
allowFrom: createAllowFromSection({
|
||||
helpTitle: "Telegram user id",
|
||||
helpLines: TELEGRAM_USER_ID_HELP_LINES,
|
||||
credentialInputKey: "token",
|
||||
@ -102,7 +103,7 @@ export const telegramSetupWizard: ChannelSetupWizard = {
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
dmPolicy,
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@ export type ResolvedWhatsAppAccount = {
|
||||
enabled: boolean;
|
||||
sendReadReceipts: boolean;
|
||||
messagePrefix?: string;
|
||||
defaultTo?: string;
|
||||
authDir: string;
|
||||
isLegacyAuthDir: boolean;
|
||||
selfChatMode?: boolean;
|
||||
@ -135,6 +136,7 @@ export function resolveWhatsAppAccount(params: {
|
||||
sendReadReceipts: accountCfg?.sendReadReceipts ?? rootCfg?.sendReadReceipts ?? true,
|
||||
messagePrefix:
|
||||
accountCfg?.messagePrefix ?? rootCfg?.messagePrefix ?? params.cfg.messages?.messagePrefix,
|
||||
defaultTo: accountCfg?.defaultTo ?? rootCfg?.defaultTo,
|
||||
authDir,
|
||||
isLegacyAuthDir: isLegacy,
|
||||
selfChatMode: accountCfg?.selfChatMode ?? rootCfg?.selfChatMode,
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyRouteAllowlistWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import { createDelegatedSetupWizardProxy } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatWhatsAppConfigAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
resolveWhatsAppConfigAllowFrom,
|
||||
resolveWhatsAppConfigDefaultTo,
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
WhatsAppConfigSchema,
|
||||
type ChannelPlugin,
|
||||
@ -33,17 +33,40 @@ export async function loadWhatsAppChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(async () => ({
|
||||
whatsappSetupWizard: (await loadWhatsAppChannelRuntime()).whatsappSetupWizard,
|
||||
}));
|
||||
export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(
|
||||
async () => (await loadWhatsAppChannelRuntime()).whatsappSetupWizard,
|
||||
);
|
||||
|
||||
const whatsappConfigBase = createScopedChannelConfigBase<ResolvedWhatsAppAccount>({
|
||||
sectionKey: WHATSAPP_CHANNEL,
|
||||
listAccountIds: listWhatsAppAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultWhatsAppAccountId,
|
||||
clearBaseFields: [],
|
||||
allowTopLevel: false,
|
||||
});
|
||||
|
||||
const whatsappConfigAccessors = createScopedAccountConfigAccessors<ResolvedWhatsAppAccount>({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveWhatsAppAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatWhatsAppConfigAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: (account) => account.defaultTo,
|
||||
});
|
||||
|
||||
const whatsappResolveDmPolicy = createScopedDmSecurityResolver<ResolvedWhatsAppAccount>({
|
||||
channelKey: WHATSAPP_CHANNEL,
|
||||
resolvePolicy: (account) => account.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw),
|
||||
});
|
||||
|
||||
export function createWhatsAppSetupWizardProxy(
|
||||
loadWizard: () => Promise<{
|
||||
whatsappSetupWizard: NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["setupWizard"]>;
|
||||
}>,
|
||||
loadWizard: () => Promise<NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["setupWizard"]>>,
|
||||
): NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["setupWizard"]> {
|
||||
return {
|
||||
return createDelegatedSetupWizardProxy({
|
||||
channel: WHATSAPP_CHANNEL,
|
||||
loadWizard,
|
||||
status: {
|
||||
configuredLabel: "linked",
|
||||
unconfiguredLabel: "not linked",
|
||||
@ -51,20 +74,11 @@ export function createWhatsAppSetupWizardProxy(
|
||||
unconfiguredHint: "not linked",
|
||||
configuredScore: 5,
|
||||
unconfiguredScore: 4,
|
||||
resolveConfigured: async ({ cfg }) =>
|
||||
await (await loadWizard()).whatsappSetupWizard.status.resolveConfigured({ cfg }),
|
||||
resolveStatusLines: async ({ cfg, configured }) =>
|
||||
(await (
|
||||
await loadWizard()
|
||||
).whatsappSetupWizard.status.resolveStatusLines?.({
|
||||
cfg,
|
||||
configured,
|
||||
})) ?? [],
|
||||
},
|
||||
resolveShouldPromptAccountIds: (params) =>
|
||||
(params.shouldPromptAccountIds || params.options?.promptWhatsAppAccountId) ?? false,
|
||||
credentials: [],
|
||||
finalize: async (params) => await (await loadWizard()).whatsappSetupWizard.finalize!(params),
|
||||
delegateFinalize: true,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
@ -78,7 +92,7 @@ export function createWhatsAppSetupWizardProxy(
|
||||
onAccountRecorded: (accountId, options) => {
|
||||
options?.onWhatsAppAccountId?.(accountId);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createWhatsAppPluginBase(params: {
|
||||
@ -119,45 +133,7 @@ export function createWhatsAppPluginBase(params: {
|
||||
gatewayMethods: ["web.login.start", "web.login.wait"],
|
||||
configSchema: buildChannelConfigSchema(WhatsAppConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listWhatsAppAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
const accountKey = accountId || DEFAULT_ACCOUNT_ID;
|
||||
const accounts = { ...cfg.channels?.whatsapp?.accounts };
|
||||
const existing = accounts[accountKey] ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
whatsapp: {
|
||||
...cfg.channels?.whatsapp,
|
||||
accounts: {
|
||||
...accounts,
|
||||
[accountKey]: {
|
||||
...existing,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
const accountKey = accountId || DEFAULT_ACCOUNT_ID;
|
||||
const accounts = { ...cfg.channels?.whatsapp?.accounts };
|
||||
delete accounts[accountKey];
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
whatsapp: {
|
||||
...cfg.channels?.whatsapp,
|
||||
accounts: Object.keys(accounts).length ? accounts : undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
...whatsappConfigBase,
|
||||
isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
|
||||
disabledReason: () => "disabled",
|
||||
isConfigured: params.isConfigured,
|
||||
@ -171,22 +147,10 @@ export function createWhatsAppPluginBase(params: {
|
||||
dmPolicy: account.dmPolicy,
|
||||
allowFrom: account.allowFrom,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppConfigAllowFrom({ cfg, accountId }),
|
||||
formatAllowFrom: ({ allowFrom }) => formatWhatsAppConfigAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: ({ cfg, accountId }) => resolveWhatsAppConfigDefaultTo({ cfg, accountId }),
|
||||
...whatsappConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: WHATSAPP_CHANNEL,
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.dmPolicy,
|
||||
allowFrom: account.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw),
|
||||
}),
|
||||
resolveDmPolicy: whatsappResolveDmPolicy,
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const groupAllowlistConfigured =
|
||||
Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0;
|
||||
|
||||
128
src/channels/plugins/setup-wizard-binary.test.ts
Normal file
128
src/channels/plugins/setup-wizard-binary.test.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createCliPathTextInput,
|
||||
createDelegatedSetupWizardStatusResolvers,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
createDetectedBinaryStatus,
|
||||
} from "./setup-wizard-binary.js";
|
||||
import type { ChannelSetupWizard } from "./setup-wizard.js";
|
||||
|
||||
describe("createDetectedBinaryStatus", () => {
|
||||
it("builds status lines, hint, and score from binary detection", async () => {
|
||||
const status = createDetectedBinaryStatus({
|
||||
channelLabel: "Signal",
|
||||
binaryLabel: "signal-cli",
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "signal-cli found",
|
||||
unconfiguredHint: "signal-cli missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: () => true,
|
||||
resolveBinaryPath: () => "/usr/local/bin/signal-cli",
|
||||
detectBinary: vi.fn(async () => true),
|
||||
});
|
||||
|
||||
expect(await status.resolveConfigured({ cfg: {} })).toBe(true);
|
||||
expect(await status.resolveStatusLines?.({ cfg: {}, configured: true })).toEqual([
|
||||
"Signal: configured",
|
||||
"signal-cli: found (/usr/local/bin/signal-cli)",
|
||||
]);
|
||||
expect(await status.resolveSelectionHint?.({ cfg: {}, configured: true })).toBe(
|
||||
"signal-cli found",
|
||||
);
|
||||
expect(await status.resolveQuickstartScore?.({ cfg: {}, configured: true })).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createCliPathTextInput", () => {
|
||||
it("reuses the same path resolver for current and initial values", async () => {
|
||||
const textInput = createCliPathTextInput({
|
||||
inputKey: "cliPath",
|
||||
message: "CLI path",
|
||||
resolvePath: () => "imsg",
|
||||
shouldPrompt: async () => false,
|
||||
helpTitle: "iMessage",
|
||||
helpLines: ["help"],
|
||||
});
|
||||
|
||||
expect(
|
||||
await textInput.currentValue?.({ cfg: {}, accountId: "default", credentialValues: {} }),
|
||||
).toBe("imsg");
|
||||
expect(
|
||||
await textInput.initialValue?.({ cfg: {}, accountId: "default", credentialValues: {} }),
|
||||
).toBe("imsg");
|
||||
expect(textInput.helpTitle).toBe("iMessage");
|
||||
expect(textInput.helpLines).toEqual(["help"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegatedSetupWizardStatusResolvers", () => {
|
||||
it("forwards optional status resolvers to the loaded wizard", async () => {
|
||||
const loadWizard = vi.fn(
|
||||
async (): Promise<ChannelSetupWizard> => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
resolveStatusLines: async () => ["line"],
|
||||
resolveSelectionHint: async () => "hint",
|
||||
resolveQuickstartScore: async () => 7,
|
||||
},
|
||||
credentials: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const status = createDelegatedSetupWizardStatusResolvers(loadWizard);
|
||||
|
||||
expect(await status.resolveStatusLines?.({ cfg: {}, configured: true })).toEqual(["line"]);
|
||||
expect(await status.resolveSelectionHint?.({ cfg: {}, configured: true })).toBe("hint");
|
||||
expect(await status.resolveQuickstartScore?.({ cfg: {}, configured: true })).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegatedTextInputShouldPrompt", () => {
|
||||
it("forwards shouldPrompt for the requested input key", async () => {
|
||||
const loadWizard = vi.fn(
|
||||
async (): Promise<ChannelSetupWizard> => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "cliPath",
|
||||
message: "CLI path",
|
||||
shouldPrompt: async ({ currentValue }) => currentValue !== "imsg",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const shouldPrompt = createDelegatedTextInputShouldPrompt({
|
||||
loadWizard,
|
||||
inputKey: "cliPath",
|
||||
});
|
||||
|
||||
expect(
|
||||
await shouldPrompt({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
currentValue: "imsg",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await shouldPrompt({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
currentValue: "other",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
100
src/channels/plugins/setup-wizard-binary.ts
Normal file
100
src/channels/plugins/setup-wizard-binary.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { detectBinary as defaultDetectBinary } from "../../plugins/setup-binary.js";
|
||||
import type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardStatus,
|
||||
ChannelSetupWizardTextInput,
|
||||
} from "./setup-wizard.js";
|
||||
|
||||
type SetupTextInputParams = Parameters<NonNullable<ChannelSetupWizardTextInput["currentValue"]>>[0];
|
||||
type SetupStatusParams = Parameters<NonNullable<ChannelSetupWizardStatus["resolveStatusLines"]>>[0];
|
||||
|
||||
export function createDetectedBinaryStatus(params: {
|
||||
channelLabel: string;
|
||||
binaryLabel: string;
|
||||
configuredLabel: string;
|
||||
unconfiguredLabel: string;
|
||||
configuredHint: string;
|
||||
unconfiguredHint: string;
|
||||
configuredScore: number;
|
||||
unconfiguredScore: number;
|
||||
resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise<boolean>;
|
||||
resolveBinaryPath: (params: { cfg: OpenClawConfig }) => string;
|
||||
detectBinary?: (path: string) => Promise<boolean>;
|
||||
}): ChannelSetupWizardStatus {
|
||||
const detectBinary = params.detectBinary ?? defaultDetectBinary;
|
||||
return {
|
||||
configuredLabel: params.configuredLabel,
|
||||
unconfiguredLabel: params.unconfiguredLabel,
|
||||
configuredHint: params.configuredHint,
|
||||
unconfiguredHint: params.unconfiguredHint,
|
||||
configuredScore: params.configuredScore,
|
||||
unconfiguredScore: params.unconfiguredScore,
|
||||
resolveConfigured: params.resolveConfigured,
|
||||
resolveStatusLines: async ({ cfg, configured }: SetupStatusParams) => {
|
||||
const binaryPath = params.resolveBinaryPath({ cfg });
|
||||
const detected = await detectBinary(binaryPath);
|
||||
return [
|
||||
`${params.channelLabel}: ${configured ? params.configuredLabel : params.unconfiguredLabel}`,
|
||||
`${params.binaryLabel}: ${detected ? "found" : "missing"} (${binaryPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) =>
|
||||
(await detectBinary(params.resolveBinaryPath({ cfg })))
|
||||
? params.configuredHint
|
||||
: params.unconfiguredHint,
|
||||
resolveQuickstartScore: async ({ cfg }) =>
|
||||
(await detectBinary(params.resolveBinaryPath({ cfg })))
|
||||
? params.configuredScore
|
||||
: params.unconfiguredScore,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCliPathTextInput(params: {
|
||||
inputKey: ChannelSetupWizardTextInput["inputKey"];
|
||||
message: string;
|
||||
resolvePath: (params: SetupTextInputParams) => string | undefined;
|
||||
shouldPrompt: NonNullable<ChannelSetupWizardTextInput["shouldPrompt"]>;
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
}): ChannelSetupWizardTextInput {
|
||||
return {
|
||||
inputKey: params.inputKey,
|
||||
message: params.message,
|
||||
currentValue: params.resolvePath,
|
||||
initialValue: params.resolvePath,
|
||||
shouldPrompt: params.shouldPrompt,
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDelegatedSetupWizardStatusResolvers(
|
||||
loadWizard: () => Promise<ChannelSetupWizard>,
|
||||
): Pick<
|
||||
ChannelSetupWizardStatus,
|
||||
"resolveStatusLines" | "resolveSelectionHint" | "resolveQuickstartScore"
|
||||
> {
|
||||
return {
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).status.resolveQuickstartScore?.(params),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDelegatedTextInputShouldPrompt(params: {
|
||||
loadWizard: () => Promise<ChannelSetupWizard>;
|
||||
inputKey: ChannelSetupWizardTextInput["inputKey"];
|
||||
}): NonNullable<ChannelSetupWizardTextInput["shouldPrompt"]> {
|
||||
return async (inputParams) => {
|
||||
const input = (await params.loadWizard()).textInputs?.find(
|
||||
(entry) => entry.inputKey === params.inputKey,
|
||||
);
|
||||
return (await input?.shouldPrompt?.(inputParams)) ?? false;
|
||||
};
|
||||
}
|
||||
@ -6,6 +6,7 @@ import {
|
||||
buildSingleChannelSecretPromptState,
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createAllowFromSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createNestedChannelAllowFromSetter,
|
||||
createNestedChannelDmPolicy,
|
||||
@ -25,6 +26,7 @@ import {
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptLegacyChannelAllowFrom,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
promptParsedAllowFromForAccount,
|
||||
parseSetupEntriesWithParser,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptSingleChannelSecretInput,
|
||||
@ -33,6 +35,7 @@ import {
|
||||
resolveAccountIdForConfigure,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveGroupAllowlistWithLookupNotes,
|
||||
resolveParsedAllowFromEntries,
|
||||
resolveSetupAccountId,
|
||||
setAccountDmAllowFromForChannel,
|
||||
setAccountAllowFromForChannel,
|
||||
@ -582,6 +585,76 @@ describe("promptParsedAllowFromForScopedChannel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptParsedAllowFromForAccount", () => {
|
||||
it("applies parsed allowFrom values through the provided writer", async () => {
|
||||
const prompter = createPrompter(["Alice, ALICE"]);
|
||||
|
||||
const next = await promptParsedAllowFromForAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
accounts: {
|
||||
alt: {
|
||||
allowFrom: ["old"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
accountId: "alt",
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
prompter,
|
||||
noteTitle: "BlueBubbles allowlist",
|
||||
noteLines: ["line"],
|
||||
message: "msg",
|
||||
placeholder: "placeholder",
|
||||
parseEntries: (raw) =>
|
||||
parseSetupEntriesWithParser(raw, (entry) => ({ value: entry.toLowerCase() })),
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId]?.allowFrom ?? [],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: "bluebubbles",
|
||||
accountId,
|
||||
patch: { allowFrom },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(next.channels?.bluebubbles?.accounts?.alt?.allowFrom).toEqual(["alice"]);
|
||||
expect(prompter.note).toHaveBeenCalledWith("line", "BlueBubbles allowlist");
|
||||
});
|
||||
|
||||
it("can merge parsed values with existing entries", async () => {
|
||||
const next = await promptParsedAllowFromForAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
nostr: {
|
||||
allowFrom: ["old"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
prompter: createPrompter(["new"]),
|
||||
noteTitle: "Nostr allowlist",
|
||||
noteLines: ["line"],
|
||||
message: "msg",
|
||||
placeholder: "placeholder",
|
||||
parseEntries: (raw) => ({ entries: [raw.trim()] }),
|
||||
getExistingAllowFrom: ({ cfg }) => cfg.channels?.nostr?.allowFrom ?? [],
|
||||
mergeEntries: ({ existing, parsed }) => [...existing.map(String), ...parsed],
|
||||
applyAllowFrom: ({ cfg, allowFrom }) =>
|
||||
patchTopLevelChannelConfigSection({
|
||||
cfg,
|
||||
channel: "nostr",
|
||||
patch: { allowFrom },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(next.channels?.nostr?.allowFrom).toEqual(["old", "new"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("channel lookup note helpers", () => {
|
||||
it("emits summary lines for resolved and unresolved entries", async () => {
|
||||
const prompter = { note: vi.fn(async () => undefined) };
|
||||
@ -1402,6 +1475,44 @@ describe("createAccountScopedAllowFromSection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAllowFromSection", () => {
|
||||
it("builds a parsed allowFrom section with default local resolution", async () => {
|
||||
const section = createAllowFromSection({
|
||||
helpTitle: "LINE allowlist",
|
||||
helpLines: ["line"],
|
||||
credentialInputKey: "token",
|
||||
message: "LINE allowFrom",
|
||||
placeholder: "U123",
|
||||
invalidWithoutCredentialNote: "need ids",
|
||||
parseId: (value) => value.trim().toUpperCase() || null,
|
||||
apply: ({ cfg, accountId, allowFrom }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: "line",
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(section.helpTitle).toBe("LINE allowlist");
|
||||
await expect(
|
||||
section.resolveEntries({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
credentialValues: {},
|
||||
entries: ["u1"],
|
||||
}),
|
||||
).resolves.toEqual([{ input: "u1", resolved: true, id: "U1" }]);
|
||||
|
||||
const next = await section.apply({
|
||||
cfg: {},
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
allowFrom: ["U1"],
|
||||
});
|
||||
expect(next.channels?.line?.allowFrom).toEqual(["U1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAccountScopedGroupAccessSection", () => {
|
||||
it("builds group access with shared setPolicy and fallback lookup notes", async () => {
|
||||
const prompter = createPrompter([]);
|
||||
@ -1544,6 +1655,20 @@ describe("resolveEntriesWithOptionalToken", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveParsedAllowFromEntries", () => {
|
||||
it("maps parsed ids into resolved/unresolved entries", () => {
|
||||
expect(
|
||||
resolveParsedAllowFromEntries({
|
||||
entries: ["alice", " "],
|
||||
parseId: (raw) => raw.trim() || null,
|
||||
}),
|
||||
).toEqual([
|
||||
{ input: "alice", resolved: true, id: "alice" },
|
||||
{ input: " ", resolved: false, id: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseMentionOrPrefixedId", () => {
|
||||
it("parses mention ids", () => {
|
||||
expect(
|
||||
|
||||
@ -16,7 +16,7 @@ import type {
|
||||
PromptAccountId,
|
||||
PromptAccountIdParams,
|
||||
} from "./setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "./setup-wizard.js";
|
||||
import type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry } from "./setup-wizard.js";
|
||||
|
||||
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
|
||||
const existingIds = params.listAccountIds(params.cfg);
|
||||
@ -1051,9 +1051,8 @@ export async function promptSingleChannelSecretInput(params: {
|
||||
|
||||
type ParsedAllowFromResult = { entries: string[]; error?: string };
|
||||
|
||||
export async function promptParsedAllowFromForScopedChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "imessage" | "signal";
|
||||
export async function promptParsedAllowFromForAccount<TConfig extends OpenClawConfig>(params: {
|
||||
cfg: TConfig;
|
||||
accountId?: string;
|
||||
defaultAccountId: string;
|
||||
prompter: Pick<WizardPrompter, "note" | "text">;
|
||||
@ -1062,11 +1061,14 @@ export async function promptParsedAllowFromForScopedChannel(params: {
|
||||
message: string;
|
||||
placeholder: string;
|
||||
parseEntries: (raw: string) => ParsedAllowFromResult;
|
||||
getExistingAllowFrom: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
getExistingAllowFrom: (params: { cfg: TConfig; accountId: string }) => Array<string | number>;
|
||||
mergeEntries?: (params: { existing: Array<string | number>; parsed: string[] }) => string[];
|
||||
applyAllowFrom: (params: {
|
||||
cfg: TConfig;
|
||||
accountId: string;
|
||||
}) => Array<string | number>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
allowFrom: string[];
|
||||
}) => TConfig | Promise<TConfig>;
|
||||
}): Promise<TConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: params.defaultAccountId,
|
||||
@ -1089,15 +1091,97 @@ export async function promptParsedAllowFromForScopedChannel(params: {
|
||||
},
|
||||
});
|
||||
const parsed = params.parseEntries(String(entry));
|
||||
const unique = mergeAllowFromEntries(undefined, parsed.entries);
|
||||
return setAccountAllowFromForChannel({
|
||||
const unique =
|
||||
params.mergeEntries?.({
|
||||
existing,
|
||||
parsed: parsed.entries,
|
||||
}) ?? mergeAllowFromEntries(undefined, parsed.entries);
|
||||
return await params.applyAllowFrom({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
accountId,
|
||||
allowFrom: unique,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptParsedAllowFromForScopedChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "imessage" | "signal";
|
||||
accountId?: string;
|
||||
defaultAccountId: string;
|
||||
prompter: Pick<WizardPrompter, "note" | "text">;
|
||||
noteTitle: string;
|
||||
noteLines: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
parseEntries: (raw: string) => ParsedAllowFromResult;
|
||||
getExistingAllowFrom: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
}) => Array<string | number>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return await promptParsedAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: params.defaultAccountId,
|
||||
prompter: params.prompter,
|
||||
noteTitle: params.noteTitle,
|
||||
noteLines: params.noteLines,
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
parseEntries: params.parseEntries,
|
||||
getExistingAllowFrom: params.getExistingAllowFrom,
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
setAccountAllowFromForChannel({
|
||||
cfg,
|
||||
channel: params.channel,
|
||||
accountId,
|
||||
allowFrom,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveParsedAllowFromEntries(params: {
|
||||
entries: string[];
|
||||
parseId: (raw: string) => string | null;
|
||||
}): ChannelSetupWizardAllowFromEntry[] {
|
||||
return params.entries.map((entry) => {
|
||||
const id = params.parseId(entry);
|
||||
return {
|
||||
input: entry,
|
||||
resolved: Boolean(id),
|
||||
id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createAllowFromSection(params: {
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
invalidWithoutCredentialNote: string;
|
||||
parseInputs?: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseInputs"]>;
|
||||
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
||||
resolveEntries?: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
||||
apply: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["apply"]>;
|
||||
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
||||
return {
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
||||
...(params.parseInputs ? { parseInputs: params.parseInputs } : {}),
|
||||
parseId: params.parseId,
|
||||
resolveEntries:
|
||||
params.resolveEntries ??
|
||||
(async ({ entries }) => resolveParsedAllowFromEntries({ entries, parseId: params.parseId })),
|
||||
apply: params.apply,
|
||||
};
|
||||
}
|
||||
|
||||
export async function noteChannelLookupSummary(params: {
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
label: string;
|
||||
|
||||
266
src/channels/plugins/setup-wizard-proxy.test.ts
Normal file
266
src/channels/plugins/setup-wizard-proxy.test.ts
Normal file
@ -0,0 +1,266 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createAllowlistSetupWizardProxy,
|
||||
createDelegatedFinalize,
|
||||
createDelegatedPrepare,
|
||||
createDelegatedResolveConfigured,
|
||||
createDelegatedSetupWizardProxy,
|
||||
} from "./setup-wizard-proxy.js";
|
||||
import type { ChannelSetupWizard } from "./setup-wizard.js";
|
||||
|
||||
describe("createDelegatedResolveConfigured", () => {
|
||||
it("forwards configured resolution to the loaded wizard", async () => {
|
||||
const loadWizard = vi.fn(
|
||||
async (): Promise<ChannelSetupWizard> => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: async ({ cfg }) => Boolean(cfg.channels?.demo),
|
||||
},
|
||||
credentials: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const resolveConfigured = createDelegatedResolveConfigured(loadWizard);
|
||||
|
||||
expect(await resolveConfigured({ cfg: {} })).toBe(false);
|
||||
expect(await resolveConfigured({ cfg: { channels: { demo: {} } } })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegatedPrepare", () => {
|
||||
it("forwards prepare when the loaded wizard implements it", async () => {
|
||||
const loadWizard = vi.fn(
|
||||
async (): Promise<ChannelSetupWizard> => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
},
|
||||
credentials: [],
|
||||
prepare: async ({ cfg }) => ({ cfg: { ...cfg, channels: { demo: { enabled: true } } } }),
|
||||
}),
|
||||
);
|
||||
|
||||
const prepare = createDelegatedPrepare(loadWizard);
|
||||
|
||||
expect(
|
||||
await prepare({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
runtime: {} as never,
|
||||
prompter: {} as never,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegatedFinalize", () => {
|
||||
it("forwards finalize when the loaded wizard implements it", async () => {
|
||||
const loadWizard = vi.fn(
|
||||
async (): Promise<ChannelSetupWizard> => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
},
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, forceAllowFrom }) => ({
|
||||
cfg: {
|
||||
...cfg,
|
||||
channels: {
|
||||
demo: { forceAllowFrom },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const finalize = createDelegatedFinalize(loadWizard);
|
||||
|
||||
expect(
|
||||
await finalize({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
runtime: {} as never,
|
||||
prompter: {} as never,
|
||||
forceAllowFrom: true,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: { forceAllowFrom: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAllowlistSetupWizardProxy", () => {
|
||||
it("falls back when delegated surfaces are absent", async () => {
|
||||
const wizard = createAllowlistSetupWizardProxy({
|
||||
loadWizard: async () =>
|
||||
({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
},
|
||||
credentials: [],
|
||||
}) satisfies ChannelSetupWizard,
|
||||
createBase: ({ promptAllowFrom, resolveAllowFromEntries, resolveGroupAllowlist }) => ({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
resolveConfigured: () => true,
|
||||
},
|
||||
credentials: [],
|
||||
dmPolicy: {
|
||||
label: "Demo",
|
||||
channel: "demo" as never,
|
||||
policyKey: "channels.demo.dmPolicy",
|
||||
allowFromKey: "channels.demo.allowFrom",
|
||||
getCurrent: () => "pairing",
|
||||
setPolicy: (cfg) => cfg,
|
||||
promptAllowFrom,
|
||||
},
|
||||
allowFrom: {
|
||||
message: "Allow from",
|
||||
placeholder: "id",
|
||||
invalidWithoutCredentialNote: "need id",
|
||||
parseId: () => null,
|
||||
resolveEntries: resolveAllowFromEntries,
|
||||
apply: (params) => params.cfg,
|
||||
},
|
||||
groupAccess: {
|
||||
label: "Groups",
|
||||
placeholder: "group",
|
||||
currentPolicy: () => "allowlist",
|
||||
currentEntries: () => [],
|
||||
updatePrompt: () => false,
|
||||
setPolicy: (params) => params.cfg,
|
||||
resolveAllowlist: resolveGroupAllowlist,
|
||||
},
|
||||
}),
|
||||
fallbackResolvedGroupAllowlist: (entries) => entries.map((input) => ({ input })),
|
||||
});
|
||||
|
||||
expect(
|
||||
await wizard.dmPolicy?.promptAllowFrom?.({
|
||||
cfg: {},
|
||||
prompter: {} as never,
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual({});
|
||||
expect(
|
||||
await wizard.allowFrom?.resolveEntries({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
entries: ["alice"],
|
||||
}),
|
||||
).toEqual([{ input: "alice", resolved: false, id: null }]);
|
||||
expect(
|
||||
await wizard.groupAccess?.resolveAllowlist?.({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
entries: ["general"],
|
||||
prompter: {} as never,
|
||||
}),
|
||||
).toEqual([{ input: "general" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegatedSetupWizardProxy", () => {
|
||||
it("builds a direct proxy wizard with delegated status/prepare/finalize", async () => {
|
||||
const wizard = createDelegatedSetupWizardProxy({
|
||||
channel: "demo",
|
||||
loadWizard: async () =>
|
||||
({
|
||||
channel: "demo",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "ready",
|
||||
unconfiguredHint: "missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: async ({ cfg }) => Boolean(cfg.channels?.demo),
|
||||
resolveStatusLines: async () => ["line"],
|
||||
resolveSelectionHint: async () => "hint",
|
||||
resolveQuickstartScore: async () => 3,
|
||||
},
|
||||
credentials: [],
|
||||
prepare: async ({ cfg }) => ({
|
||||
cfg: { ...cfg, channels: { demo: { prepared: true } } },
|
||||
}),
|
||||
finalize: async ({ cfg }) => ({
|
||||
cfg: { ...cfg, channels: { demo: { finalized: true } } },
|
||||
}),
|
||||
}) satisfies ChannelSetupWizard,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "ready",
|
||||
unconfiguredHint: "missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [],
|
||||
completionNote: { title: "Done", lines: ["line"] },
|
||||
delegatePrepare: true,
|
||||
delegateFinalize: true,
|
||||
});
|
||||
|
||||
expect(await wizard.status.resolveConfigured({ cfg: {} })).toBe(false);
|
||||
expect(await wizard.status.resolveStatusLines?.({ cfg: {}, configured: false })).toEqual([
|
||||
"line",
|
||||
]);
|
||||
expect(
|
||||
await wizard.prepare?.({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
runtime: {} as never,
|
||||
prompter: {} as never,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: { prepared: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await wizard.finalize?.({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
credentialValues: {},
|
||||
runtime: {} as never,
|
||||
prompter: {} as never,
|
||||
forceAllowFrom: false,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: { finalized: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,8 +1,10 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createDelegatedSetupWizardStatusResolvers } from "./setup-wizard-binary.js";
|
||||
import type { ChannelSetupDmPolicy } from "./setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "./setup-wizard.js";
|
||||
|
||||
type PromptAllowFromParams = Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0];
|
||||
type ResolveConfiguredParams = Parameters<ChannelSetupWizard["status"]["resolveConfigured"]>[0];
|
||||
type ResolveAllowFromEntriesParams = Parameters<
|
||||
NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]
|
||||
>[0];
|
||||
@ -13,6 +15,61 @@ type ResolveGroupAllowlistParams = Parameters<
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>[0];
|
||||
|
||||
export function createDelegatedResolveConfigured(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return async ({ cfg }: ResolveConfiguredParams) =>
|
||||
await (await loadWizard()).status.resolveConfigured({ cfg });
|
||||
}
|
||||
|
||||
export function createDelegatedPrepare(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return async (params: Parameters<NonNullable<ChannelSetupWizard["prepare"]>>[0]) =>
|
||||
await (await loadWizard()).prepare?.(params);
|
||||
}
|
||||
|
||||
export function createDelegatedFinalize(loadWizard: () => Promise<ChannelSetupWizard>) {
|
||||
return async (params: Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]) =>
|
||||
await (await loadWizard()).finalize?.(params);
|
||||
}
|
||||
|
||||
type DelegatedStatusBase = Omit<
|
||||
ChannelSetupWizard["status"],
|
||||
"resolveConfigured" | "resolveStatusLines" | "resolveSelectionHint" | "resolveQuickstartScore"
|
||||
>;
|
||||
|
||||
export function createDelegatedSetupWizardProxy(params: {
|
||||
channel: string;
|
||||
loadWizard: () => Promise<ChannelSetupWizard>;
|
||||
status: DelegatedStatusBase;
|
||||
credentials?: ChannelSetupWizard["credentials"];
|
||||
textInputs?: ChannelSetupWizard["textInputs"];
|
||||
completionNote?: ChannelSetupWizard["completionNote"];
|
||||
dmPolicy?: ChannelSetupWizard["dmPolicy"];
|
||||
disable?: ChannelSetupWizard["disable"];
|
||||
resolveShouldPromptAccountIds?: ChannelSetupWizard["resolveShouldPromptAccountIds"];
|
||||
onAccountRecorded?: ChannelSetupWizard["onAccountRecorded"];
|
||||
delegatePrepare?: boolean;
|
||||
delegateFinalize?: boolean;
|
||||
}): ChannelSetupWizard {
|
||||
return {
|
||||
channel: params.channel,
|
||||
status: {
|
||||
...params.status,
|
||||
resolveConfigured: createDelegatedResolveConfigured(params.loadWizard),
|
||||
...createDelegatedSetupWizardStatusResolvers(params.loadWizard),
|
||||
},
|
||||
...(params.resolveShouldPromptAccountIds
|
||||
? { resolveShouldPromptAccountIds: params.resolveShouldPromptAccountIds }
|
||||
: {}),
|
||||
...(params.delegatePrepare ? { prepare: createDelegatedPrepare(params.loadWizard) } : {}),
|
||||
credentials: params.credentials ?? [],
|
||||
...(params.textInputs ? { textInputs: params.textInputs } : {}),
|
||||
...(params.delegateFinalize ? { finalize: createDelegatedFinalize(params.loadWizard) } : {}),
|
||||
...(params.completionNote ? { completionNote: params.completionNote } : {}),
|
||||
...(params.dmPolicy ? { dmPolicy: params.dmPolicy } : {}),
|
||||
...(params.disable ? { disable: params.disable } : {}),
|
||||
...(params.onAccountRecorded ? { onAccountRecorded: params.onAccountRecorded } : {}),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
|
||||
export function createAllowlistSetupWizardProxy<TGroupResolved>(params: {
|
||||
loadWizard: () => Promise<ChannelSetupWizard>;
|
||||
createBase: (handlers: {
|
||||
|
||||
@ -110,6 +110,54 @@ describe("createScopedChannelConfigBase", () => {
|
||||
}).channels,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can force default account config into accounts.default", () => {
|
||||
const base = createScopedChannelConfigBase({
|
||||
sectionKey: "demo",
|
||||
listAccountIds: () => ["default", "alt"],
|
||||
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
||||
defaultAccountId: () => "default",
|
||||
clearBaseFields: [],
|
||||
allowTopLevel: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
base.setAccountEnabled!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
token: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
}).channels?.demo,
|
||||
).toEqual({
|
||||
token: "secret",
|
||||
accounts: {
|
||||
default: { enabled: true },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
base.deleteAccount!({
|
||||
cfg: {
|
||||
channels: {
|
||||
demo: {
|
||||
token: "secret",
|
||||
accounts: {
|
||||
default: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
}).channels?.demo,
|
||||
).toEqual({
|
||||
token: "secret",
|
||||
accounts: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createScopedDmSecurityResolver", () => {
|
||||
|
||||
@ -35,6 +35,7 @@ export {
|
||||
buildSingleChannelSecretPromptState,
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createAllowFromSection,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
createNestedChannelAllowFromSetter,
|
||||
createNestedChannelDmPolicy,
|
||||
@ -55,13 +56,16 @@ export {
|
||||
patchChannelConfigForAccount,
|
||||
promptLegacyChannelAllowFrom,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
promptParsedAllowFromForAccount,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
promptSingleChannelSecretInput,
|
||||
promptResolvedAllowFrom,
|
||||
resolveParsedAllowFromEntries,
|
||||
resolveEntriesWithOptionalToken,
|
||||
resolveSetupAccountId,
|
||||
resolveGroupAllowlistWithLookupNotes,
|
||||
runSingleChannelSecretStep,
|
||||
setAccountAllowFromForChannel,
|
||||
setAccountDmAllowFromForChannel,
|
||||
setAccountGroupPolicyForChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
@ -75,5 +79,17 @@ export {
|
||||
splitSetupEntries,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
export {
|
||||
createDelegatedFinalize,
|
||||
createDelegatedPrepare,
|
||||
createDelegatedResolveConfigured,
|
||||
createDelegatedSetupWizardProxy,
|
||||
} from "../channels/plugins/setup-wizard-proxy.js";
|
||||
export {
|
||||
createCliPathTextInput,
|
||||
createDelegatedSetupWizardStatusResolvers,
|
||||
createDelegatedTextInputShouldPrompt,
|
||||
createDetectedBinaryStatus,
|
||||
} from "../channels/plugins/setup-wizard-binary.js";
|
||||
|
||||
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
|
||||
|
||||
@ -99,6 +99,15 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof setupSdk.DEFAULT_ACCOUNT_ID).toBe("string");
|
||||
expect(typeof setupSdk.createAccountScopedAllowFromSection).toBe("function");
|
||||
expect(typeof setupSdk.createAccountScopedGroupAccessSection).toBe("function");
|
||||
expect(typeof setupSdk.createAllowFromSection).toBe("function");
|
||||
expect(typeof setupSdk.createCliPathTextInput).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedFinalize).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedPrepare).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedResolveConfigured).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedSetupWizardProxy).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedSetupWizardStatusResolvers).toBe("function");
|
||||
expect(typeof setupSdk.createDelegatedTextInputShouldPrompt).toBe("function");
|
||||
expect(typeof setupSdk.createDetectedBinaryStatus).toBe("function");
|
||||
expect(typeof setupSdk.createLegacyCompatChannelDmPolicy).toBe("function");
|
||||
expect(typeof setupSdk.createNestedChannelDmPolicy).toBe("function");
|
||||
expect(typeof setupSdk.createTopLevelChannelDmPolicy).toBe("function");
|
||||
@ -107,7 +116,10 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof setupSdk.mergeAllowFromEntries).toBe("function");
|
||||
expect(typeof setupSdk.patchNestedChannelConfigSection).toBe("function");
|
||||
expect(typeof setupSdk.patchTopLevelChannelConfigSection).toBe("function");
|
||||
expect(typeof setupSdk.promptParsedAllowFromForAccount).toBe("function");
|
||||
expect(typeof setupSdk.resolveParsedAllowFromEntries).toBe("function");
|
||||
expect(typeof setupSdk.resolveGroupAllowlistWithLookupNotes).toBe("function");
|
||||
expect(typeof setupSdk.setAccountAllowFromForChannel).toBe("function");
|
||||
expect(typeof setupSdk.setAccountDmAllowFromForChannel).toBe("function");
|
||||
expect(typeof setupSdk.setTopLevelChannelDmPolicyWithAllowFrom).toBe("function");
|
||||
expect(typeof setupSdk.formatResolvedUnresolvedNote).toBe("function");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user