Files
openclaw/extensions/imessage/src/onboarding.ts
2026-03-15 15:59:12 -07:00

184 lines
6.1 KiB
TypeScript

import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../../../src/channels/plugins/onboarding-types.js";
import {
parseOnboardingEntriesAllowingWildcard,
patchChannelConfigForAccount,
promptParsedAllowFromForScopedChannel,
resolveAccountIdForConfigure,
setChannelDmPolicyWithAllowFrom,
setOnboardingChannelEnabled,
} from "../../../src/channels/plugins/onboarding/helpers.js";
import { detectBinary } from "../../../src/commands/onboard-helpers.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { formatDocsLink } from "../../../src/terminal/links.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "./accounts.js";
import { normalizeIMessageHandle } from "./targets.js";
const channel = "imessage" as const;
export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } {
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
const lower = entry.toLowerCase();
if (lower.startsWith("chat_id:")) {
const id = entry.slice("chat_id:".length).trim();
if (!/^\d+$/.test(id)) {
return { error: `Invalid chat_id: ${entry}` };
}
return { value: entry };
}
if (lower.startsWith("chat_guid:")) {
if (!entry.slice("chat_guid:".length).trim()) {
return { error: "Invalid chat_guid entry" };
}
return { value: entry };
}
if (lower.startsWith("chat_identifier:")) {
if (!entry.slice("chat_identifier:".length).trim()) {
return { error: "Invalid chat_identifier entry" };
}
return { value: entry };
}
if (!normalizeIMessageHandle(entry)) {
return { error: `Invalid handle: ${entry}` };
}
return { value: entry };
});
}
async function promptIMessageAllowFrom(params: {
cfg: OpenClawConfig;
prompter: WizardPrompter;
accountId?: string;
}): Promise<OpenClawConfig> {
return promptParsedAllowFromForScopedChannel({
cfg: params.cfg,
channel: "imessage",
accountId: params.accountId,
defaultAccountId: resolveDefaultIMessageAccountId(params.cfg),
prompter: params.prompter,
noteTitle: "iMessage allowlist",
noteLines: [
"Allowlist iMessage DMs by handle or chat target.",
"Examples:",
"- +15555550123",
"- user@example.com",
"- chat_id:123",
"- chat_guid:... or chat_identifier:...",
"Multiple entries: comma-separated.",
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
],
message: "iMessage allowFrom (handle or chat_id)",
placeholder: "+15555550123, user@example.com, chat_id:123",
parseEntries: parseIMessageAllowFromEntries,
getExistingAllowFrom: ({ cfg, accountId }) => {
const resolved = resolveIMessageAccount({ cfg, accountId });
return resolved.config.allowFrom ?? [];
},
});
}
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "iMessage",
channel,
policyKey: "channels.imessage.dmPolicy",
allowFromKey: "channels.imessage.allowFrom",
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
setPolicy: (cfg, policy) =>
setChannelDmPolicyWithAllowFrom({
cfg,
channel: "imessage",
dmPolicy: policy,
}),
promptAllowFrom: promptIMessageAllowFrom,
};
export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
getStatus: async ({ cfg }) => {
const configured = listIMessageAccountIds(cfg).some((accountId) => {
const account = resolveIMessageAccount({ cfg, accountId });
return Boolean(
account.config.cliPath ||
account.config.dbPath ||
account.config.allowFrom ||
account.config.service ||
account.config.region,
);
});
const imessageCliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
const imessageCliDetected = await detectBinary(imessageCliPath);
return {
channel,
configured,
statusLines: [
`iMessage: ${configured ? "configured" : "needs setup"}`,
`imsg: ${imessageCliDetected ? "found" : "missing"} (${imessageCliPath})`,
],
selectionHint: imessageCliDetected ? "imsg found" : "imsg missing",
quickstartScore: imessageCliDetected ? 1 : 0,
};
},
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
const defaultIMessageAccountId = resolveDefaultIMessageAccountId(cfg);
const imessageAccountId = await resolveAccountIdForConfigure({
cfg,
prompter,
label: "iMessage",
accountOverride: accountOverrides.imessage,
shouldPromptAccountIds,
listAccountIds: listIMessageAccountIds,
defaultAccountId: defaultIMessageAccountId,
});
let next = cfg;
const resolvedAccount = resolveIMessageAccount({
cfg: next,
accountId: imessageAccountId,
});
let resolvedCliPath = resolvedAccount.config.cliPath ?? "imsg";
const cliDetected = await detectBinary(resolvedCliPath);
if (!cliDetected) {
const entered = await prompter.text({
message: "imsg CLI path",
initialValue: resolvedCliPath,
validate: (value) => (value?.trim() ? undefined : "Required"),
});
resolvedCliPath = String(entered).trim();
if (!resolvedCliPath) {
await prompter.note("imsg CLI path required to enable iMessage.", "iMessage");
}
}
if (resolvedCliPath) {
next = patchChannelConfigForAccount({
cfg: next,
channel: "imessage",
accountId: imessageAccountId,
patch: { cliPath: resolvedCliPath },
});
}
await prompter.note(
[
"This is still a work in progress.",
"Ensure OpenClaw has Full Disk Access to Messages DB.",
"Grant Automation permission for Messages when prompted.",
"List chats with: imsg chats --limit 20",
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
].join("\n"),
"iMessage next steps",
);
return { cfg: next, accountId: imessageAccountId };
},
dmPolicy,
disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false),
};