mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 12:11:34 +00:00
* fix(imessage): recognize bare hex group chat identifiers as chat targets A 32-char hex iMessage group chat identifier (as returned by `imsg` for group chats) with no explicit `chat_identifier:` prefix fell through to E.164 phone-number normalization. normalizeE164 strips non-digit characters and prepends `+`, turning `7d5297154d5f436d83dbbdf03fcc8fdd` into `+75297154543683038` -- a bogus phone number. Cron/announce delivery then reported `delivered: true` while silently sending to a nonexistent recipient. Detect bare 32-char hex strings in both the send-path target parser (extensions/imessage/src/targets.ts) and the outbound delivery-target normalizer (extensions/imessage/src/normalize.ts) and route them as chat_identifier targets, consistent with how an explicit `chat_identifier:<id>` prefix already resolves. Fixes #89235. * refactor(imessage): centralize bare chat identifiers * refactor(imessage): centralize bare chat identifiers --------- Co-authored-by: Peter Steinberger <peter@steipete.me> Co-authored-by: Peter Steinberger <steipete@gmail.com>
227 lines
7.0 KiB
TypeScript
227 lines
7.0 KiB
TypeScript
// Imessage plugin module implements targets behavior.
|
|
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { normalizeBareIMessageChatIdentifier } from "./target-identifiers.js";
|
|
import {
|
|
type ChatSenderAllowParams,
|
|
createAllowedChatSenderMatcher,
|
|
type ParsedChatTarget,
|
|
parseChatTargetPrefixesOrThrow,
|
|
resolveServicePrefixedChatTarget,
|
|
resolveServicePrefixedOrChatAllowTarget,
|
|
} from "./target-parsing-helpers.js";
|
|
|
|
export type IMessageService = "imessage" | "sms" | "auto";
|
|
|
|
export type IMessageTarget =
|
|
| { kind: "chat_id"; chatId: number }
|
|
| { kind: "chat_guid"; chatGuid: string }
|
|
| { kind: "chat_identifier"; chatIdentifier: string }
|
|
| { kind: "handle"; to: string; service: IMessageService; serviceExplicit?: boolean };
|
|
|
|
export type IMessageAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string };
|
|
|
|
const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
|
|
const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
|
|
const CHAT_IDENTIFIER_PREFIXES = ["chat_identifier:", "chatidentifier:", "chatident:"];
|
|
const SERVICE_PREFIXES: Array<{ prefix: string; service: IMessageService }> = [
|
|
{ prefix: "imessage:", service: "imessage" },
|
|
{ prefix: "sms:", service: "sms" },
|
|
{ prefix: "auto:", service: "auto" },
|
|
];
|
|
|
|
function parseServicePrefixedBareChatIdentifier(params: {
|
|
trimmed: string;
|
|
lower: string;
|
|
}): IMessageTarget | undefined {
|
|
for (const { prefix } of SERVICE_PREFIXES) {
|
|
if (!params.lower.startsWith(prefix)) {
|
|
continue;
|
|
}
|
|
const chatIdentifier = normalizeBareIMessageChatIdentifier(params.trimmed.slice(prefix.length));
|
|
if (chatIdentifier) {
|
|
return { kind: "chat_identifier", chatIdentifier };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function normalizeIMessageHandle(raw: string): string {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
const lowered = normalizeLowercaseStringOrEmpty(trimmed);
|
|
if (lowered.startsWith("imessage:")) {
|
|
return normalizeIMessageHandle(trimmed.slice(9));
|
|
}
|
|
if (lowered.startsWith("sms:")) {
|
|
return normalizeIMessageHandle(trimmed.slice(4));
|
|
}
|
|
if (lowered.startsWith("auto:")) {
|
|
return normalizeIMessageHandle(trimmed.slice(5));
|
|
}
|
|
|
|
// Normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively
|
|
for (const prefix of CHAT_ID_PREFIXES) {
|
|
if (lowered.startsWith(prefix)) {
|
|
const value = trimmed.slice(prefix.length).trim();
|
|
return `chat_id:${value}`;
|
|
}
|
|
}
|
|
for (const prefix of CHAT_GUID_PREFIXES) {
|
|
if (lowered.startsWith(prefix)) {
|
|
const value = trimmed.slice(prefix.length).trim();
|
|
return `chat_guid:${value}`;
|
|
}
|
|
}
|
|
for (const prefix of CHAT_IDENTIFIER_PREFIXES) {
|
|
if (lowered.startsWith(prefix)) {
|
|
const value = trimmed.slice(prefix.length).trim();
|
|
return `chat_identifier:${value}`;
|
|
}
|
|
}
|
|
|
|
if (trimmed.includes("@")) {
|
|
return normalizeLowercaseStringOrEmpty(trimmed);
|
|
}
|
|
const bareChatIdentifier = normalizeBareIMessageChatIdentifier(trimmed);
|
|
if (bareChatIdentifier) {
|
|
return `chat_identifier:${bareChatIdentifier}`;
|
|
}
|
|
const normalized = normalizeE164(trimmed);
|
|
if (normalized) {
|
|
return normalized;
|
|
}
|
|
return trimmed.replace(/\s+/g, "");
|
|
}
|
|
|
|
export function parseIMessageTarget(raw: string): IMessageTarget {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
throw new Error("iMessage target is required");
|
|
}
|
|
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
|
|
|
const servicePrefixedBareChatIdentifier = parseServicePrefixedBareChatIdentifier({
|
|
trimmed,
|
|
lower,
|
|
});
|
|
if (servicePrefixedBareChatIdentifier) {
|
|
return servicePrefixedBareChatIdentifier;
|
|
}
|
|
|
|
const servicePrefixed = resolveServicePrefixedChatTarget({
|
|
trimmed,
|
|
lower,
|
|
servicePrefixes: SERVICE_PREFIXES,
|
|
chatIdPrefixes: CHAT_ID_PREFIXES,
|
|
chatGuidPrefixes: CHAT_GUID_PREFIXES,
|
|
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
|
|
parseTarget: parseIMessageTarget,
|
|
});
|
|
if (servicePrefixed) {
|
|
if (servicePrefixed.kind === "handle") {
|
|
return { ...servicePrefixed, serviceExplicit: true };
|
|
}
|
|
return servicePrefixed;
|
|
}
|
|
|
|
const chatTarget = parseChatTargetPrefixesOrThrow({
|
|
trimmed,
|
|
lower,
|
|
chatIdPrefixes: CHAT_ID_PREFIXES,
|
|
chatGuidPrefixes: CHAT_GUID_PREFIXES,
|
|
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
|
|
});
|
|
if (chatTarget) {
|
|
return chatTarget;
|
|
}
|
|
|
|
const bareChatIdentifier = normalizeBareIMessageChatIdentifier(trimmed);
|
|
if (bareChatIdentifier) {
|
|
return { kind: "chat_identifier", chatIdentifier: bareChatIdentifier };
|
|
}
|
|
|
|
return { kind: "handle", to: trimmed, service: "auto" };
|
|
}
|
|
|
|
export function looksLikeIMessageExplicitTargetId(raw: string): boolean {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
return false;
|
|
}
|
|
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
|
if (/^(imessage:|sms:|auto:)/.test(lower)) {
|
|
return true;
|
|
}
|
|
return (
|
|
CHAT_ID_PREFIXES.some((prefix) => lower.startsWith(prefix)) ||
|
|
CHAT_GUID_PREFIXES.some((prefix) => lower.startsWith(prefix)) ||
|
|
CHAT_IDENTIFIER_PREFIXES.some((prefix) => lower.startsWith(prefix)) ||
|
|
Boolean(normalizeBareIMessageChatIdentifier(trimmed))
|
|
);
|
|
}
|
|
|
|
export function inferIMessageTargetChatType(raw: string): "direct" | "group" | undefined {
|
|
try {
|
|
const parsed = parseIMessageTarget(raw);
|
|
if (parsed.kind === "handle") {
|
|
return "direct";
|
|
}
|
|
return "group";
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
return { kind: "handle", handle: "" };
|
|
}
|
|
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
|
|
|
const servicePrefixed = resolveServicePrefixedOrChatAllowTarget({
|
|
trimmed,
|
|
lower,
|
|
servicePrefixes: SERVICE_PREFIXES,
|
|
parseAllowTarget: parseIMessageAllowTarget,
|
|
chatIdPrefixes: CHAT_ID_PREFIXES,
|
|
chatGuidPrefixes: CHAT_GUID_PREFIXES,
|
|
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
|
|
});
|
|
if (servicePrefixed) {
|
|
return servicePrefixed;
|
|
}
|
|
|
|
return { kind: "handle", handle: normalizeIMessageHandle(trimmed) };
|
|
}
|
|
|
|
const isAllowedIMessageSenderMatcher = createAllowedChatSenderMatcher({
|
|
normalizeSender: normalizeIMessageHandle,
|
|
parseAllowTarget: parseIMessageAllowTarget,
|
|
allowConversationTargets: false,
|
|
});
|
|
|
|
export function isAllowedIMessageSender(params: ChatSenderAllowParams): boolean {
|
|
return isAllowedIMessageSenderMatcher({ ...params, allowConversationTargets: false });
|
|
}
|
|
|
|
const isAllowedIMessageReplyContextSenderMatcher = createAllowedChatSenderMatcher({
|
|
normalizeSender: normalizeIMessageHandle,
|
|
parseAllowTarget: parseIMessageAllowTarget,
|
|
allowConversationTargets: true,
|
|
});
|
|
|
|
export function isAllowedIMessageReplyContextSender(params: ChatSenderAllowParams): boolean {
|
|
return isAllowedIMessageReplyContextSenderMatcher(params);
|
|
}
|
|
|
|
export function formatIMessageChatTarget(chatId?: number | null): string {
|
|
if (!chatId || !Number.isFinite(chatId)) {
|
|
return "";
|
|
}
|
|
return `chat_id:${chatId}`;
|
|
}
|