Files
openclaw/extensions/imessage/src/monitor/parse-notification.ts
T
Omar Shahine 8c7ac9b9b9 fix(imessage): native poll vote-cue, echo suppression, and same-sender comment fold
Makes native iMessage polls behave correctly end to end.

What changed:
- Inbound polls render with a numbered-options vote cue so the agent casts a native vote instead of answering the poll in prose.
- poll-vote resolves the poll reference from pollId/pollGuid/messageId and now defaults to the current inbound poll message when the model omits it; still errors when no reference exists.
- poll-vote echo suppression is session-scoped, so the redundant spoken answer is dropped across the separate poll and comment runs.
- A poll's inline-reply caption is folded (not delivered as a standalone question) only when the poll creator and reply sender are both known and equal; unknown/mismatched sender falls through to the normal inbound decision gate, so no inbound reply is silently dropped.

Evidence:
- 64 passing tests in the poll suites (poll-comment, poll-render, actions), incl. sender fail-closed regressions; pnpm build clean.
- Two Codex autoreviews clean (patch is correct); ClawSweeper re-review rated it platinum hermit.
- Live-verified on macOS 26.4.1 on the deployed gateway: poll "What color pill?" -> native vote delivered with a 7333-byte payload, caption folded, zero echo.

Note: vote delivery also depends on the imsg vote-stamp fix (openclaw/imsg#150); OpenClaw ships ahead of imsg per owner decision.
2026-07-01 21:30:29 -07:00

107 lines
3.6 KiB
TypeScript

// Imessage plugin module implements parse notification behavior.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { stripImessageLengthPrefixedUtf8Text } from "./strip-imsg-length-prefixed-text.js";
import type { IMessagePayload } from "./types.js";
function isOptionalString(value: unknown): value is string | null | undefined {
return value === undefined || value === null || typeof value === "string";
}
function isOptionalStringOrNumber(value: unknown): value is string | number | null | undefined {
return (
value === undefined || value === null || typeof value === "string" || typeof value === "number"
);
}
function isOptionalNumber(value: unknown): value is number | null | undefined {
return value === undefined || value === null || typeof value === "number";
}
function isOptionalBoolean(value: unknown): value is boolean | null | undefined {
return value === undefined || value === null || typeof value === "boolean";
}
function isOptionalStringArray(value: unknown): value is string[] | null | undefined {
return (
value === undefined ||
value === null ||
(Array.isArray(value) && value.every((entry) => typeof entry === "string"))
);
}
function isOptionalAttachments(value: unknown): value is IMessagePayload["attachments"] {
if (value === undefined || value === null) {
return true;
}
if (!Array.isArray(value)) {
return false;
}
return value.every((attachment) => {
if (!isRecord(attachment)) {
return false;
}
return (
isOptionalString(attachment.original_path) &&
isOptionalString(attachment.mime_type) &&
isOptionalBoolean(attachment.missing) &&
isOptionalString(attachment.transfer_name) &&
isOptionalString(attachment.uti)
);
});
}
export function parseIMessageNotification(raw: unknown): IMessagePayload | null {
if (!isRecord(raw)) {
return null;
}
const maybeMessage = raw.message;
if (!isRecord(maybeMessage)) {
return null;
}
const message: IMessagePayload = maybeMessage;
if (
!isOptionalNumber(message.id) ||
!isOptionalString(message.guid) ||
!isOptionalNumber(message.chat_id) ||
!isOptionalString(message.sender) ||
!isOptionalString(message.destination_caller_id) ||
!isOptionalString(message.balloon_bundle_id) ||
!isOptionalBoolean(message.is_from_me) ||
!isOptionalString(message.text) ||
!isOptionalStringOrNumber(message.reply_to_id) ||
!isOptionalString(message.reply_to_guid) ||
!isOptionalString(message.reply_to_text) ||
!isOptionalString(message.reply_to_sender) ||
!isOptionalString(message.created_at) ||
!isOptionalBoolean(message.is_reaction) ||
!isOptionalBoolean(message.is_tapback) ||
!isOptionalString(message.associated_message_guid) ||
!isOptionalNumber(message.associated_message_type) ||
!isOptionalString(message.reaction_type) ||
!isOptionalString(message.reaction_emoji) ||
!isOptionalBoolean(message.is_reaction_add) ||
!isOptionalString(message.reacted_to_guid) ||
!isOptionalAttachments(message.attachments) ||
!isOptionalString(message.chat_identifier) ||
!isOptionalString(message.chat_guid) ||
!isOptionalString(message.chat_name) ||
!isOptionalStringArray(message.participants) ||
!isOptionalBoolean(message.is_group)
) {
return null;
}
return {
...message,
text:
typeof message.text === "string"
? stripImessageLengthPrefixedUtf8Text(message.text)
: message.text,
reply_to_text:
typeof message.reply_to_text === "string"
? stripImessageLengthPrefixedUtf8Text(message.reply_to_text)
: message.reply_to_text,
};
}