mirror of https://github.com/openclaw/openclaw.git
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import {
|
|
mergeAllowFromEntries,
|
|
resolveSetupAccountId,
|
|
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
|
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
|
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
|
import type { OpenClawConfig } from "../../../src/config/config.js";
|
|
import type { DmPolicy } from "../../../src/config/types.js";
|
|
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
|
import { formatDocsLink } from "../../../src/terminal/links.js";
|
|
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
|
import {
|
|
listBlueBubblesAccountIds,
|
|
resolveBlueBubblesAccount,
|
|
resolveDefaultBlueBubblesAccountId,
|
|
} from "./accounts.js";
|
|
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
|
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
|
|
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
|
import {
|
|
blueBubblesSetupAdapter,
|
|
setBlueBubblesAllowFrom,
|
|
setBlueBubblesDmPolicy,
|
|
} from "./setup-core.js";
|
|
import { parseBlueBubblesAllowTarget } from "./targets.js";
|
|
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
|
|
|
const channel = "bluebubbles" as const;
|
|
const CONFIGURE_CUSTOM_WEBHOOK_FLAG = "__bluebubblesConfigureCustomWebhookPath";
|
|
|
|
function parseBlueBubblesAllowFromInput(raw: string): string[] {
|
|
return raw
|
|
.split(/[\n,]+/g)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function validateBlueBubblesAllowFromEntry(value: string): string | null {
|
|
try {
|
|
if (value === "*") {
|
|
return value;
|
|
}
|
|
const parsed = parseBlueBubblesAllowTarget(value);
|
|
if (parsed.kind === "handle" && !parsed.handle) {
|
|
return null;
|
|
}
|
|
return value.trim() || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function promptBlueBubblesAllowFrom(params: {
|
|
cfg: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
accountId?: string;
|
|
}): Promise<OpenClawConfig> {
|
|
const accountId = resolveSetupAccountId({
|
|
accountId: params.accountId,
|
|
defaultAccountId: resolveDefaultBlueBubblesAccountId(params.cfg),
|
|
});
|
|
const resolved = resolveBlueBubblesAccount({ cfg: params.cfg, accountId });
|
|
const existing = resolved.config.allowFrom ?? [];
|
|
await params.prompter.note(
|
|
[
|
|
"Allowlist BlueBubbles DMs by handle or chat target.",
|
|
"Examples:",
|
|
"- +15555550123",
|
|
"- user@example.com",
|
|
"- chat_id:123",
|
|
"- 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}`;
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
const parts = parseBlueBubblesAllowFromInput(String(entry));
|
|
const unique = mergeAllowFromEntries(undefined, parts);
|
|
return setBlueBubblesAllowFrom(params.cfg, accountId, unique);
|
|
}
|
|
|
|
function validateBlueBubblesServerUrlInput(value: unknown): string | undefined {
|
|
const trimmed = String(value ?? "").trim();
|
|
if (!trimmed) {
|
|
return "Required";
|
|
}
|
|
try {
|
|
const normalized = normalizeBlueBubblesServerUrl(trimmed);
|
|
new URL(normalized);
|
|
return undefined;
|
|
} catch {
|
|
return "Invalid URL format";
|
|
}
|
|
}
|
|
|
|
function applyBlueBubblesSetupPatch(
|
|
cfg: OpenClawConfig,
|
|
accountId: string,
|
|
patch: {
|
|
serverUrl?: string;
|
|
password?: unknown;
|
|
webhookPath?: string;
|
|
},
|
|
): OpenClawConfig {
|
|
return applyBlueBubblesConnectionConfig({
|
|
cfg,
|
|
accountId,
|
|
patch,
|
|
onlyDefinedFields: true,
|
|
accountEnabled: "preserve-or-true",
|
|
});
|
|
}
|
|
|
|
function resolveBlueBubblesServerUrl(cfg: OpenClawConfig, accountId: string): string | undefined {
|
|
return resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl?.trim() || undefined;
|
|
}
|
|
|
|
function resolveBlueBubblesWebhookPath(cfg: OpenClawConfig, accountId: string): string | undefined {
|
|
return resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath?.trim() || undefined;
|
|
}
|
|
|
|
function validateBlueBubblesWebhookPath(value: string): string | undefined {
|
|
const trimmed = String(value ?? "").trim();
|
|
if (!trimmed) {
|
|
return "Required";
|
|
}
|
|
if (!trimmed.startsWith("/")) {
|
|
return "Path must start with /";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const dmPolicy: ChannelSetupDmPolicy = {
|
|
label: "BlueBubbles",
|
|
channel,
|
|
policyKey: "channels.bluebubbles.dmPolicy",
|
|
allowFromKey: "channels.bluebubbles.allowFrom",
|
|
getCurrent: (cfg) => cfg.channels?.bluebubbles?.dmPolicy ?? "pairing",
|
|
setPolicy: (cfg, policy) => setBlueBubblesDmPolicy(cfg, policy),
|
|
promptAllowFrom: promptBlueBubblesAllowFrom,
|
|
};
|
|
|
|
export const blueBubblesSetupWizard: ChannelSetupWizard = {
|
|
channel,
|
|
stepOrder: "text-first",
|
|
status: {
|
|
configuredLabel: "configured",
|
|
unconfiguredLabel: "needs setup",
|
|
configuredHint: "configured",
|
|
unconfiguredHint: "iMessage via BlueBubbles app",
|
|
configuredScore: 1,
|
|
unconfiguredScore: 0,
|
|
resolveConfigured: ({ cfg }) =>
|
|
listBlueBubblesAccountIds(cfg).some((accountId) => {
|
|
const account = resolveBlueBubblesAccount({ cfg, accountId });
|
|
return account.configured;
|
|
}),
|
|
resolveStatusLines: ({ configured }) => [
|
|
`BlueBubbles: ${configured ? "configured" : "needs setup"}`,
|
|
],
|
|
resolveSelectionHint: ({ configured }) =>
|
|
configured ? "configured" : "iMessage via BlueBubbles app",
|
|
},
|
|
prepare: async ({ cfg, accountId, prompter, credentialValues }) => {
|
|
const existingWebhookPath = resolveBlueBubblesWebhookPath(cfg, accountId);
|
|
const wantsCustomWebhook = await prompter.confirm({
|
|
message: `Configure a custom webhook path? (default: ${DEFAULT_WEBHOOK_PATH})`,
|
|
initialValue: Boolean(existingWebhookPath && existingWebhookPath !== DEFAULT_WEBHOOK_PATH),
|
|
});
|
|
return {
|
|
cfg: wantsCustomWebhook
|
|
? cfg
|
|
: applyBlueBubblesSetupPatch(cfg, accountId, { webhookPath: DEFAULT_WEBHOOK_PATH }),
|
|
credentialValues: {
|
|
...credentialValues,
|
|
[CONFIGURE_CUSTOM_WEBHOOK_FLAG]: wantsCustomWebhook ? "1" : "0",
|
|
},
|
|
};
|
|
},
|
|
credentials: [
|
|
{
|
|
inputKey: "password",
|
|
providerHint: channel,
|
|
credentialLabel: "server password",
|
|
helpTitle: "BlueBubbles password",
|
|
helpLines: [
|
|
"Enter the BlueBubbles server password.",
|
|
"Find this in the BlueBubbles Server app under Settings.",
|
|
],
|
|
envPrompt: "",
|
|
keepPrompt: "BlueBubbles password already set. Keep it?",
|
|
inputPrompt: "BlueBubbles password",
|
|
inspect: ({ cfg, accountId }) => {
|
|
const existingPassword = resolveBlueBubblesAccount({ cfg, accountId }).config.password;
|
|
return {
|
|
accountConfigured: resolveBlueBubblesAccount({ cfg, accountId }).configured,
|
|
hasConfiguredValue: hasConfiguredSecretInput(existingPassword),
|
|
resolvedValue: normalizeSecretInputString(existingPassword) ?? undefined,
|
|
};
|
|
},
|
|
applySet: async ({ cfg, accountId, value }) =>
|
|
applyBlueBubblesSetupPatch(cfg, accountId, {
|
|
password: value,
|
|
}),
|
|
},
|
|
],
|
|
textInputs: [
|
|
{
|
|
inputKey: "httpUrl",
|
|
message: "BlueBubbles server URL",
|
|
placeholder: "http://192.168.1.100:1234",
|
|
helpTitle: "BlueBubbles server URL",
|
|
helpLines: [
|
|
"Enter the BlueBubbles server URL (e.g., http://192.168.1.100:1234).",
|
|
"Find this in the BlueBubbles Server app under Connection.",
|
|
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
|
],
|
|
currentValue: ({ cfg, accountId }) => resolveBlueBubblesServerUrl(cfg, accountId),
|
|
validate: ({ value }) => validateBlueBubblesServerUrlInput(value),
|
|
normalizeValue: ({ value }) => String(value).trim(),
|
|
applySet: async ({ cfg, accountId, value }) =>
|
|
applyBlueBubblesSetupPatch(cfg, accountId, {
|
|
serverUrl: value,
|
|
}),
|
|
},
|
|
{
|
|
inputKey: "webhookPath",
|
|
message: "Webhook path",
|
|
placeholder: DEFAULT_WEBHOOK_PATH,
|
|
currentValue: ({ cfg, accountId }) => {
|
|
const value = resolveBlueBubblesWebhookPath(cfg, accountId);
|
|
return value && value !== DEFAULT_WEBHOOK_PATH ? value : undefined;
|
|
},
|
|
shouldPrompt: ({ credentialValues }) =>
|
|
credentialValues[CONFIGURE_CUSTOM_WEBHOOK_FLAG] === "1",
|
|
validate: ({ value }) => validateBlueBubblesWebhookPath(value),
|
|
normalizeValue: ({ value }) => String(value).trim(),
|
|
applySet: async ({ cfg, accountId, value }) =>
|
|
applyBlueBubblesSetupPatch(cfg, accountId, {
|
|
webhookPath: value,
|
|
}),
|
|
},
|
|
],
|
|
completionNote: {
|
|
title: "BlueBubbles next steps",
|
|
lines: [
|
|
"Configure the webhook URL in BlueBubbles Server:",
|
|
"1. Open BlueBubbles Server -> Settings -> Webhooks",
|
|
"2. Add your OpenClaw gateway URL + webhook path",
|
|
` Example: https://your-gateway-host:3000${DEFAULT_WEBHOOK_PATH}`,
|
|
"3. Enable the webhook and save",
|
|
"",
|
|
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
|
],
|
|
},
|
|
dmPolicy,
|
|
allowFrom: {
|
|
helpTitle: "BlueBubbles allowlist",
|
|
helpLines: [
|
|
"Allowlist BlueBubbles DMs by handle or chat target.",
|
|
"Examples:",
|
|
"- +15555550123",
|
|
"- user@example.com",
|
|
"- chat_id:123",
|
|
"- chat_guid:iMessage;-;+15555550123",
|
|
"Multiple entries: comma- or newline-separated.",
|
|
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
|
],
|
|
message: "BlueBubbles allowFrom (handle or chat_id)",
|
|
placeholder: "+15555550123, user@example.com, chat_id:123",
|
|
invalidWithoutCredentialNote:
|
|
"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: {
|
|
...cfg.channels,
|
|
bluebubbles: {
|
|
...cfg.channels?.bluebubbles,
|
|
enabled: false,
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
|
|
export { blueBubblesSetupAdapter };
|