openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
354 lines (353 loc) • 11.1 kB
JavaScript
import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs, v as parseStrictInteger } from "./number-coercion-CJQ8TR--.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js";
import "./temp-path-CeQpRzl5.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as createIMessageRpcClient } from "./client-RDkCLFWE.js";
import { c as resolveIMessageMessageId } from "./monitor-reply-cache-DUH5oJzE.js";
import { n as appendIMessageCliStderrTail, r as appendIMessageCliStdout, t as extractMarkdownFormatRuns } from "./markdown-format-41-kk349.js";
import { extname, join } from "node:path";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
//#region extensions/imessage/src/actions.runtime.ts
function asChatList(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
const chats = value.chats;
if (!Array.isArray(chats)) return [];
return chats.filter((chat) => chat != null && typeof chat === "object" && !Array.isArray(chat));
}
function numberFromUnknown(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
return parseStrictInteger(value);
}
function stringFromUnknown(value) {
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
const CHAT_LIST_CACHE_TTL_MS = 30 * 1e3;
const chatListCache = /* @__PURE__ */ new Map();
function chatListCacheKey(cliPath, dbPath) {
return `${cliPath}\0${dbPath ?? ""}`;
}
function chatListCacheGet(cliPath, dbPath) {
const key = chatListCacheKey(cliPath, dbPath);
const entry = chatListCache.get(key);
if (!entry) return null;
const now = asDateTimestampMs(Date.now());
if (now === void 0 || entry.expiresAt <= now) {
chatListCache.delete(key);
return null;
}
return entry.list;
}
function chatListCacheSet(cliPath, dbPath, list) {
const expiresAt = resolveExpiresAtMsFromDurationMs(CHAT_LIST_CACHE_TTL_MS);
if (expiresAt === void 0) return;
chatListCache.set(chatListCacheKey(cliPath, dbPath), {
list,
expiresAt
});
}
/**
* Strip the iMessage;-;/SMS;-;/any;-; service prefix that Messages uses
* for direct DM chats. Different layers report direct DMs in different
* forms — the action surface synthesizes `iMessage;-;<phone>` from a
* handle target, while imsg's chats.list returns `identifier: <phone>`
* and `guid: any;-;<phone>`. Comparing the raw strings would falsely
* miss the match. Mirror of the same helper in monitor-reply-cache.ts.
*/
function normalizeDirectChatIdentifierForTest(raw) {
return normalizeDirectChatIdentifier(raw);
}
function findChatGuidForTest(chats, target) {
return findChatGuid(chats, target);
}
function normalizeDirectChatIdentifier(raw) {
const trimmed = raw.trim();
const lowered = trimmed.toLowerCase();
for (const prefix of [
"imessage;-;",
"sms;-;",
"any;-;"
]) if (lowered.startsWith(prefix)) return trimmed.slice(prefix.length);
return trimmed;
}
function findChatGuid(chats, target) {
if (target.kind === "chat_id") {
for (const chat of chats) {
const id = numberFromUnknown(chat.id);
const guid = stringFromUnknown(chat.guid);
if (id === target.chatId && guid) return guid;
}
return null;
}
const wanted = normalizeDirectChatIdentifier(target.chatIdentifier);
for (const chat of chats) {
const identifier = stringFromUnknown(chat.identifier);
const guid = stringFromUnknown(chat.guid);
if (!guid) continue;
if (identifier === target.chatIdentifier || guid === target.chatIdentifier || identifier && normalizeDirectChatIdentifier(identifier) === wanted || normalizeDirectChatIdentifier(guid) === wanted) return guid;
}
return null;
}
function buildIMessageCliJsonArgs(args, options) {
const dbPath = options.dbPath?.trim();
return [
...args,
...dbPath ? ["--db", dbPath] : [],
"--json"
];
}
async function runIMessageCliJson(args, options) {
return await new Promise((resolve, reject) => {
const child = spawn(options.cliPath, buildIMessageCliJsonArgs(args, options), { stdio: [
"ignore",
"pipe",
"pipe"
] });
let stdout = "";
let stderr = "";
let killEscalation = null;
let settled = false;
const clearTimers = (optionsValue = {}) => {
if (timer) clearTimeout(timer);
if (killEscalation && !optionsValue.keepKillEscalation) clearTimeout(killEscalation);
};
const fail = (error, optionsLocal = {}) => {
if (settled) return;
settled = true;
clearTimers(optionsLocal);
reject(error);
};
const succeed = (value) => {
if (settled) return;
settled = true;
clearTimers();
resolve(value);
};
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => {
child.kill("SIGTERM");
killEscalation = setTimeout(() => {
try {
child.kill("SIGKILL");
} catch {}
}, 2e3);
fail(/* @__PURE__ */ new Error(`iMessage action timed out after ${options.timeoutMs}ms`), { keepKillEscalation: true });
}, options.timeoutMs) : null;
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
if (settled) return;
const appended = appendIMessageCliStdout(stdout, chunk);
if (!appended.ok) {
try {
child.kill("SIGKILL");
} catch {}
fail(new Error(appended.message));
return;
}
stdout = appended.value;
});
child.stderr.on("data", (chunk) => {
stderr = appendIMessageCliStderrTail(stderr, chunk);
});
child.on("error", (error) => {
if (settled) {
clearTimers();
return;
}
fail(error);
});
child.on("close", (code) => {
if (settled) {
clearTimers();
return;
}
const last = normalizeStringEntries(stdout.split(/\r?\n/)).at(-1);
let parsed = null;
if (last) try {
const value = JSON.parse(last);
if (value && typeof value === "object" && !Array.isArray(value)) parsed = value;
} catch {
parsed = null;
}
if (code !== 0) {
const detail = typeof parsed?.error === "string" && parsed.error.trim() || stderr.trim() || stdout.trim() || `imsg exited with code ${code}`;
fail(new Error(detail));
return;
}
if (!parsed) {
fail(/* @__PURE__ */ new Error(`imsg returned non-JSON output: ${stdout.trim() || stderr.trim()}`));
return;
}
if (parsed.success === false) {
const error = typeof parsed.error === "string" && parsed.error.trim() ? parsed.error.trim() : "iMessage action failed";
fail(new Error(error));
return;
}
succeed(parsed);
});
});
}
function resolveMessageId(result) {
return typeof result.messageGuid === "string" && result.messageGuid.trim() || typeof result.messageId === "string" && result.messageId.trim() || typeof result.guid === "string" && result.guid.trim() || typeof result.id === "string" && result.id.trim() || "ok";
}
async function withTempFile(input, fn) {
const dir = await mkdtemp(join(resolvePreferredOpenClawTmpDir(), "openclaw-imessage-"));
const filePath = join(dir, `upload${extname(input.filename).slice(0, 16) || ".bin"}`);
try {
await writeFile(filePath, input.buffer);
return await fn(filePath);
} finally {
await rm(dir, {
recursive: true,
force: true
});
}
}
const imessageActionsRuntime = {
resolveIMessageMessageId,
async resolveChatGuidForTarget(params) {
const cached = chatListCacheGet(params.options.cliPath, params.options.dbPath);
if (cached) return findChatGuid(cached, params.target);
const client = await createIMessageRpcClient({
cliPath: params.options.cliPath,
dbPath: params.options.dbPath
});
try {
const list = asChatList(await client.request("chats.list", { limit: 1e3 }, { timeoutMs: params.options.timeoutMs }));
chatListCacheSet(params.options.cliPath, params.options.dbPath, list);
return findChatGuid(list, params.target);
} finally {
await client.stop();
}
},
async sendReaction(params) {
await runIMessageCliJson([
"tapback",
"--chat",
params.chatGuid,
"--message",
params.messageId,
"--kind",
params.reaction,
"--part",
String(params.partIndex ?? 0),
...params.remove ? ["--remove"] : []
], params.options);
},
async editMessage(params) {
await runIMessageCliJson([
"edit",
"--chat",
params.chatGuid,
"--message",
params.messageId,
"--new-text",
params.text,
"--bc-text",
params.backwardsCompatMessage ?? params.text,
"--part",
String(params.partIndex ?? 0)
], params.options);
},
async unsendMessage(params) {
await runIMessageCliJson([
"unsend",
"--chat",
params.chatGuid,
"--message",
params.messageId,
"--part",
String(params.partIndex ?? 0)
], params.options);
},
async sendRichMessage(params) {
const formatted = extractMarkdownFormatRuns(params.text);
const buildArgs = (filePath) => [
"send-rich",
"--chat",
params.chatGuid,
"--text",
formatted.text,
"--part",
String(params.partIndex ?? 0),
...params.effectId ? ["--effect", params.effectId] : [],
...params.replyToMessageId ? ["--reply-to", params.replyToMessageId] : [],
...formatted.ranges.length > 0 ? ["--format", JSON.stringify(formatted.ranges)] : [],
...filePath ? ["--file", filePath] : []
];
if (params.attachment) return await withTempFile({
buffer: params.attachment.buffer,
filename: params.attachment.filename
}, async (filePath) => {
return { messageId: resolveMessageId(await runIMessageCliJson(buildArgs(filePath), params.options)) };
});
return { messageId: resolveMessageId(await runIMessageCliJson(buildArgs(), params.options)) };
},
async renameGroup(params) {
await runIMessageCliJson([
"chat-name",
"--chat",
params.chatGuid,
"--name",
params.displayName
], params.options);
},
async setGroupIcon(params) {
await withTempFile({
buffer: params.buffer,
filename: params.filename
}, async (filePath) => {
await runIMessageCliJson([
"chat-photo",
"--chat",
params.chatGuid,
"--file",
filePath
], params.options);
});
},
async addParticipant(params) {
await runIMessageCliJson([
"chat-add-member",
"--chat",
params.chatGuid,
"--address",
params.address
], params.options);
},
async removeParticipant(params) {
await runIMessageCliJson([
"chat-remove-member",
"--chat",
params.chatGuid,
"--address",
params.address
], params.options);
},
async leaveGroup(params) {
await runIMessageCliJson([
"chat-leave",
"--chat",
params.chatGuid
], params.options);
},
async sendAttachment(params) {
return await withTempFile({
buffer: params.buffer,
filename: params.filename
}, async (filePath) => {
return { messageId: resolveMessageId(await runIMessageCliJson([
"send-attachment",
"--chat",
params.chatGuid,
"--file",
filePath,
...params.asVoice ? ["--audio"] : []
], params.options)) };
});
}
};
//#endregion
export { findChatGuidForTest, imessageActionsRuntime, normalizeDirectChatIdentifierForTest };