import { normalizeE164 } from "../utils.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 }; export type IMessageAllowTarget = | { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | { kind: "chat_identifier"; chatIdentifier: string } | { 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 stripPrefix(value: string, prefix: string): string { return value.slice(prefix.length).trim(); } export function normalizeIMessageHandle(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { return ""; } const lowered = trimmed.toLowerCase(); 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 trimmed.toLowerCase(); } 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 = trimmed.toLowerCase(); for (const { prefix, service } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); if (!remainder) { throw new Error(`${prefix} target is required`); } const remainderLower = remainder.toLowerCase(); const isChatTarget = CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) || CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)); if (isChatTarget) { return parseIMessageTarget(remainder); } return { kind: "handle", to: remainder, service }; } } for (const prefix of CHAT_ID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); if (!Number.isFinite(chatId)) { throw new Error(`Invalid chat_id: ${value}`); } return { kind: "chat_id", chatId }; } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (!value) { throw new Error("chat_guid is required"); } return { kind: "chat_guid", chatGuid: value }; } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (!value) { throw new Error("chat_identifier is required"); } return { kind: "chat_identifier", chatIdentifier: value }; } } return { kind: "handle", to: trimmed, service: "auto" }; } export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget { const trimmed = raw.trim(); if (!trimmed) { return { kind: "handle", handle: "" }; } const lower = trimmed.toLowerCase(); for (const { prefix } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); if (!remainder) { return { kind: "handle", handle: "" }; } return parseIMessageAllowTarget(remainder); } } for (const prefix of CHAT_ID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); if (Number.isFinite(chatId)) { return { kind: "chat_id", chatId }; } } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (value) { return { kind: "chat_guid", chatGuid: value }; } } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (value) { return { kind: "chat_identifier", chatIdentifier: value }; } } } return { kind: "handle", handle: normalizeIMessageHandle(trimmed) }; } export function isAllowedIMessageSender(params: { allowFrom: Array; sender: string; chatId?: number | null; chatGuid?: string | null; chatIdentifier?: string | null; }): boolean { const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); if (allowFrom.length === 0) { return true; } if (allowFrom.includes("*")) { return true; } const senderNormalized = normalizeIMessageHandle(params.sender); const chatId = params.chatId ?? undefined; const chatGuid = params.chatGuid?.trim(); const chatIdentifier = params.chatIdentifier?.trim(); for (const entry of allowFrom) { if (!entry) { continue; } const parsed = parseIMessageAllowTarget(entry); if (parsed.kind === "chat_id" && chatId !== undefined) { if (parsed.chatId === chatId) { return true; } } else if (parsed.kind === "chat_guid" && chatGuid) { if (parsed.chatGuid === chatGuid) { return true; } } else if (parsed.kind === "chat_identifier" && chatIdentifier) { if (parsed.chatIdentifier === chatIdentifier) { return true; } } else if (parsed.kind === "handle" && senderNormalized) { if (parsed.handle === senderNormalized) { return true; } } } return false; } export function formatIMessageChatTarget(chatId?: number | null): string { if (!chatId || !Number.isFinite(chatId)) { return ""; } return `chat_id:${chatId}`; }