mirror of https://github.com/openclaw/openclaw.git
237 lines
7.2 KiB
TypeScript
237 lines
7.2 KiB
TypeScript
import {
|
|
createCliPathTextInput,
|
|
createDelegatedSetupWizardProxy,
|
|
createDelegatedTextInputShouldPrompt,
|
|
createPatchedAccountSetupAdapter,
|
|
normalizeE164,
|
|
parseSetupEntriesAllowingWildcard,
|
|
promptParsedAllowFromForAccount,
|
|
setAccountAllowFromForChannel,
|
|
setChannelDmPolicyWithAllowFrom,
|
|
setSetupChannelEnabled,
|
|
type OpenClawConfig,
|
|
type WizardPrompter,
|
|
} from "openclaw/plugin-sdk/setup";
|
|
import type {
|
|
ChannelSetupAdapter,
|
|
ChannelSetupDmPolicy,
|
|
ChannelSetupWizard,
|
|
ChannelSetupWizardTextInput,
|
|
} from "openclaw/plugin-sdk/setup";
|
|
import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
|
import {
|
|
listSignalAccountIds,
|
|
resolveDefaultSignalAccountId,
|
|
resolveSignalAccount,
|
|
} from "./accounts.js";
|
|
|
|
const channel = "signal" as const;
|
|
const MIN_E164_DIGITS = 5;
|
|
const MAX_E164_DIGITS = 15;
|
|
const DIGITS_ONLY = /^\d+$/;
|
|
const INVALID_SIGNAL_ACCOUNT_ERROR =
|
|
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
|
|
|
|
export function normalizeSignalAccountInput(value: string | null | undefined): string | null {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const normalized = normalizeE164(trimmed);
|
|
const digits = normalized.slice(1);
|
|
if (!DIGITS_ONLY.test(digits)) {
|
|
return null;
|
|
}
|
|
if (digits.length < MIN_E164_DIGITS || digits.length > MAX_E164_DIGITS) {
|
|
return null;
|
|
}
|
|
return `+${digits}`;
|
|
}
|
|
|
|
function isUuidLike(value: string): boolean {
|
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
}
|
|
|
|
export function parseSignalAllowFromEntries(raw: string): { entries: string[]; error?: string } {
|
|
return parseSetupEntriesAllowingWildcard(raw, (entry) => {
|
|
if (entry.toLowerCase().startsWith("uuid:")) {
|
|
const id = entry.slice("uuid:".length).trim();
|
|
if (!id) {
|
|
return { error: "Invalid uuid entry" };
|
|
}
|
|
return { value: `uuid:${id}` };
|
|
}
|
|
if (isUuidLike(entry)) {
|
|
return { value: `uuid:${entry}` };
|
|
}
|
|
const normalized = normalizeSignalAccountInput(entry);
|
|
if (!normalized) {
|
|
return { error: `Invalid entry: ${entry}` };
|
|
}
|
|
return { value: normalized };
|
|
});
|
|
}
|
|
|
|
function buildSignalSetupPatch(input: {
|
|
signalNumber?: string;
|
|
cliPath?: string;
|
|
httpUrl?: string;
|
|
httpHost?: string;
|
|
httpPort?: string;
|
|
}) {
|
|
return {
|
|
...(input.signalNumber ? { account: input.signalNumber } : {}),
|
|
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
|
...(input.httpUrl ? { httpUrl: input.httpUrl } : {}),
|
|
...(input.httpHost ? { httpHost: input.httpHost } : {}),
|
|
...(input.httpPort ? { httpPort: Number(input.httpPort) } : {}),
|
|
};
|
|
}
|
|
|
|
export async function promptSignalAllowFrom(params: {
|
|
cfg: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
accountId?: string;
|
|
}): Promise<OpenClawConfig> {
|
|
return promptParsedAllowFromForAccount({
|
|
cfg: params.cfg,
|
|
accountId: params.accountId,
|
|
defaultAccountId: resolveDefaultSignalAccountId(params.cfg),
|
|
prompter: params.prompter,
|
|
noteTitle: "Signal allowlist",
|
|
noteLines: [
|
|
"Allowlist Signal DMs by sender id.",
|
|
"Examples:",
|
|
"- +15555550123",
|
|
"- uuid:123e4567-e89b-12d3-a456-426614174000",
|
|
"Multiple entries: comma-separated.",
|
|
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
|
],
|
|
message: "Signal allowFrom (E.164 or uuid)",
|
|
placeholder: "+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000",
|
|
parseEntries: parseSignalAllowFromEntries,
|
|
getExistingAllowFrom: ({ cfg, accountId }) =>
|
|
resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? [],
|
|
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
|
setAccountAllowFromForChannel({
|
|
cfg,
|
|
channel,
|
|
accountId,
|
|
allowFrom,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export const signalDmPolicy: ChannelSetupDmPolicy = {
|
|
label: "Signal",
|
|
channel,
|
|
policyKey: "channels.signal.dmPolicy",
|
|
allowFromKey: "channels.signal.allowFrom",
|
|
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
|
setPolicy: (cfg: OpenClawConfig, policy) =>
|
|
setChannelDmPolicyWithAllowFrom({
|
|
cfg,
|
|
channel,
|
|
dmPolicy: policy,
|
|
}),
|
|
promptAllowFrom: promptSignalAllowFrom,
|
|
};
|
|
|
|
function resolveSignalCliPath(params: {
|
|
cfg: OpenClawConfig;
|
|
accountId: string;
|
|
credentialValues: Record<string, unknown>;
|
|
}) {
|
|
return (
|
|
(typeof params.credentialValues.cliPath === "string"
|
|
? params.credentialValues.cliPath
|
|
: undefined) ??
|
|
resolveSignalAccount({ cfg: params.cfg, accountId: params.accountId }).config.cliPath ??
|
|
"signal-cli"
|
|
);
|
|
}
|
|
|
|
export function createSignalCliPathTextInput(
|
|
shouldPrompt: NonNullable<ChannelSetupWizardTextInput["shouldPrompt"]>,
|
|
): ChannelSetupWizardTextInput {
|
|
return createCliPathTextInput({
|
|
inputKey: "cliPath",
|
|
message: "signal-cli path",
|
|
resolvePath: ({ cfg, accountId, credentialValues }) =>
|
|
resolveSignalCliPath({ cfg, accountId, credentialValues }),
|
|
shouldPrompt,
|
|
helpTitle: "Signal",
|
|
helpLines: [
|
|
"signal-cli not found. Install it, then rerun this step or set channels.signal.cliPath.",
|
|
],
|
|
});
|
|
}
|
|
|
|
export const signalNumberTextInput: ChannelSetupWizardTextInput = {
|
|
inputKey: "signalNumber",
|
|
message: "Signal bot number (E.164)",
|
|
currentValue: ({ cfg, accountId }) =>
|
|
normalizeSignalAccountInput(resolveSignalAccount({ cfg, accountId }).config.account) ??
|
|
undefined,
|
|
keepPrompt: (value) => `Signal account set (${value}). Keep it?`,
|
|
validate: ({ value }) =>
|
|
normalizeSignalAccountInput(value) ? undefined : INVALID_SIGNAL_ACCOUNT_ERROR,
|
|
normalizeValue: ({ value }) => normalizeSignalAccountInput(value) ?? value,
|
|
};
|
|
|
|
export const signalCompletionNote = {
|
|
title: "Signal next steps",
|
|
lines: [
|
|
'Link device with: signal-cli link -n "OpenClaw"',
|
|
"Scan QR in Signal -> Linked Devices",
|
|
`Then run: ${formatCliCommand("openclaw gateway call channels.status --params '{\"probe\":true}'")}`,
|
|
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
|
],
|
|
};
|
|
|
|
export const signalSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
|
|
channelKey: channel,
|
|
validateInput: ({ input }) => {
|
|
if (
|
|
!input.signalNumber &&
|
|
!input.httpUrl &&
|
|
!input.httpHost &&
|
|
!input.httpPort &&
|
|
!input.cliPath
|
|
) {
|
|
return "Signal requires --signal-number or --http-url/--http-host/--http-port/--cli-path.";
|
|
}
|
|
return null;
|
|
},
|
|
buildPatch: (input) => buildSignalSetupPatch(input),
|
|
});
|
|
|
|
export function createSignalSetupWizardProxy(loadWizard: () => Promise<ChannelSetupWizard>) {
|
|
return createDelegatedSetupWizardProxy({
|
|
channel,
|
|
loadWizard,
|
|
status: {
|
|
configuredLabel: "configured",
|
|
unconfiguredLabel: "needs setup",
|
|
configuredHint: "signal-cli found",
|
|
unconfiguredHint: "signal-cli missing",
|
|
configuredScore: 1,
|
|
unconfiguredScore: 0,
|
|
},
|
|
delegatePrepare: true,
|
|
credentials: [],
|
|
textInputs: [
|
|
createSignalCliPathTextInput(
|
|
createDelegatedTextInputShouldPrompt({
|
|
loadWizard,
|
|
inputKey: "cliPath",
|
|
}),
|
|
),
|
|
signalNumberTextInput,
|
|
],
|
|
completionNote: signalCompletionNote,
|
|
dmPolicy: signalDmPolicy,
|
|
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
|
});
|
|
}
|