diff --git a/extensions/imessage/src/actions.test.ts b/extensions/imessage/src/actions.test.ts index 4ec116eefedb..b521b737853c 100644 --- a/extensions/imessage/src/actions.test.ts +++ b/extensions/imessage/src/actions.test.ts @@ -304,6 +304,52 @@ describe("imessage message actions", () => { ]); }); + it("rejects fractional chatId params before resolving chat GUIDs", async () => { + probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({ + available: true, + v2Ready: true, + selectors: {}, + }); + + await expect( + imessageMessageActions.handleAction?.({ + action: "react", + cfg: cfg(), + params: { + chatId: 42.5, + messageId: "message-guid", + emoji: "👍", + }, + } as never), + ).rejects.toThrow("chatId must be a positive integer"); + + expect(runtimeMock.resolveChatGuidForTarget).not.toHaveBeenCalled(); + expect(runtimeMock.sendReaction).not.toHaveBeenCalled(); + }); + + it("rejects fractional partIndex values before invoking bridge actions", async () => { + probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({ + available: true, + v2Ready: true, + selectors: {}, + }); + + await expect( + imessageMessageActions.handleAction?.({ + action: "react", + cfg: cfg(), + params: { + chatGuid: "iMessage;+;chat0000", + messageId: "message-guid", + emoji: "👍", + partIndex: 1.5, + }, + } as never), + ).rejects.toThrow("partIndex must be a non-negative integer"); + + expect(runtimeMock.sendReaction).not.toHaveBeenCalled(); + }); + it("resolves short message ids before invoking bridge actions", async () => { probeMock.getCachedIMessagePrivateApiStatus.mockReturnValue({ available: true, diff --git a/extensions/imessage/src/actions.ts b/extensions/imessage/src/actions.ts index aa3a534a1c72..c725730e8a2a 100644 --- a/extensions/imessage/src/actions.ts +++ b/extensions/imessage/src/actions.ts @@ -2,7 +2,8 @@ import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param"; import { createActionGate, jsonResult, - readNumberParam, + readNonNegativeIntegerParam, + readPositiveIntegerParam, readReactionParams, readStringParam, } from "openclaw/plugin-sdk/channel-actions"; @@ -103,7 +104,7 @@ async function resolveChatGuid(params: { if (explicitChatGuid) { return explicitChatGuid; } - const explicitChatId = readNumberParam(params.actionParams, "chatId", { integer: true }); + const explicitChatId = readPositiveIntegerParam(params.actionParams, "chatId"); if (typeof explicitChatId === "number") { const resolved = await params.runtime.resolveChatGuidForTarget({ target: { kind: "chat_id", chatId: explicitChatId }, @@ -198,7 +199,7 @@ function buildChatContextFromActionParams(params: { }): IMessageChatContext { const explicitChatGuid = readStringParam(params.actionParams, "chatGuid")?.trim(); const explicitChatIdentifier = readStringParam(params.actionParams, "chatIdentifier")?.trim(); - const explicitChatId = readNumberParam(params.actionParams, "chatId", { integer: true }); + const explicitChatId = readPositiveIntegerParam(params.actionParams, "chatId"); // Trim before the truthy check so a whitespace-only currentChannelId can't // reach parseIMessageTarget (which throws on empty/whitespace input and // would abort the whole action with a confusing "target is required"). @@ -486,7 +487,7 @@ export const imessageMessageActions: ChannelMessageActionAdapter = { ); } const resolvedMessageId = messageId(); - const partIndex = readNumberParam(params, "partIndex", { integer: true }); + const partIndex = readNonNegativeIntegerParam(params, "partIndex"); const resolvedChatGuid = await chatGuid(); const reactionsToSend = remove && !reaction ? [...TAPBACK_KINDS] : reaction ? [reaction] : []; for (const kind of reactionsToSend) { @@ -512,7 +513,7 @@ export const imessageMessageActions: ChannelMessageActionAdapter = { if (!text) { throw new Error("iMessage edit requires text, newText, or message."); } - const partIndex = readNumberParam(params, "partIndex", { integer: true }); + const partIndex = readNonNegativeIntegerParam(params, "partIndex"); const backwardsCompatMessage = readStringParam(params, "backwardsCompatMessage"); const resolvedChatGuid = await chatGuid(); await runtime.editMessage({ @@ -529,7 +530,7 @@ export const imessageMessageActions: ChannelMessageActionAdapter = { if (action === "unsend") { await assertPrivateApiEnabled(); const resolvedMessageId = messageId({ requireFromMe: true }); - const partIndex = readNumberParam(params, "partIndex", { integer: true }); + const partIndex = readNonNegativeIntegerParam(params, "partIndex"); const resolvedChatGuid = await chatGuid(); await runtime.unsendMessage({ chatGuid: resolvedChatGuid, @@ -569,7 +570,7 @@ export const imessageMessageActions: ChannelMessageActionAdapter = { ); } } - const partIndex = readNumberParam(params, "partIndex", { integer: true }); + const partIndex = readNonNegativeIntegerParam(params, "partIndex"); const resolvedChatGuid = await chatGuid(); const result = await runtime.sendRichMessage({ chatGuid: resolvedChatGuid,