diff --git a/extensions/imessage/src/send.test.ts b/extensions/imessage/src/send.test.ts index 884696417222..d71bdd3682fd 100644 --- a/extensions/imessage/src/send.test.ts +++ b/extensions/imessage/src/send.test.ts @@ -121,6 +121,30 @@ describe("sendMessageIMessage receipts", () => { expect(result.receipt.sentAt).toBeGreaterThan(0); }); + it("drops reply metadata from text sends when reply actions are disabled", async () => { + const client = createClient({ guid: "p:0/imsg-plain" }); + + const result = await sendMessageIMessage("chat_id:42", "hello", { + config: { + channels: { + imessage: { + actions: { reply: false }, + accounts: { default: {} }, + }, + }, + }, + client, + replyToId: "reply-1", + }); + + const sendParams = getClientMocks(client).request.mock.calls[0]?.[1] as + | Record + | undefined; + expect(sendParams).not.toHaveProperty("reply_to"); + expect(result.receipt.replyToId).toBeUndefined(); + expect(result.receipt.parts[0]?.replyToId).toBeUndefined(); + }); + it("passes the default RPC send transport", async () => { const client = createClient({ guid: "p:0/imsg-transport-default" }); @@ -299,6 +323,35 @@ describe("sendMessageIMessage receipts", () => { expect(client["request"]).not.toHaveBeenCalled(); }); + it("drops reply metadata from media sends when reply actions are disabled", async () => { + const client = createClient({ message_id: 12345 }); + const runCliJson = vi.fn().mockResolvedValueOnce({ messageId: "p:0/plain-media-guid" }); + + const result = await sendMessageIMessage("chat_guid:chat-1", "", { + config: { + channels: { + imessage: { + actions: { reply: false }, + accounts: { default: {} }, + }, + }, + }, + client, + mediaUrl: "/tmp/image.png", + replyToId: "p:0/reply-guid", + resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }), + runCliJson, + }); + + expect(result.messageId).toBe("p:0/plain-media-guid"); + expect(runCliJson.mock.calls).toEqual([ + [["send-attachment", "--chat", "chat-1", "--file", "/tmp/image.png", "--transport", "auto"]], + ]); + expect(result.receipt.replyToId).toBeUndefined(); + expect(result.receipt.parts[0]?.replyToId).toBeUndefined(); + expect(client["request"]).not.toHaveBeenCalled(); + }); + it("resolves chat_id media-only payloads before using send-attachment", async () => { const client = createClient({ message_id: 12345 }); const runCliJson = vi diff --git a/extensions/imessage/src/send.ts b/extensions/imessage/src/send.ts index 13942ef48fb4..2db30cf39efd 100644 --- a/extensions/imessage/src/send.ts +++ b/extensions/imessage/src/send.ts @@ -4,6 +4,7 @@ import { constants, accessSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; +import { createActionGate } from "openclaw/plugin-sdk/channel-actions"; import { createMessageReceiptFromOutboundResults, type MessageReceipt, @@ -948,7 +949,8 @@ export async function sendMessageIMessage( throw new Error("iMessage send requires text or media"); } const echoText = resolveOutboundEchoText(message, filePath ? mediaContentType : undefined); - const resolvedReplyToId = sanitizeReplyToId(opts.replyToId); + const replyActionsEnabled = createActionGate(account.config.actions)("reply"); + const resolvedReplyToId = replyActionsEnabled ? sanitizeReplyToId(opts.replyToId) : undefined; const runCliJson = opts.runCliJson ?? ((args: readonly string[]) => runIMessageCliJson(cliPath, dbPath, args, timeoutMs));