diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index 738e144da3..72b25087b6 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -1,3 +1,10 @@ +import { + parseChatAllowTargetPrefixes, + parseChatTargetPrefixesOrThrow, + resolveServicePrefixedAllowTarget, + resolveServicePrefixedTarget, +} from "openclaw/plugin-sdk"; + export type BlueBubblesService = "imessage" | "sms" | "auto"; export type BlueBubblesTarget = @@ -205,54 +212,30 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { } 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)) || - remainderLower.startsWith("group:"); - if (isChatTarget) { - return parseBlueBubblesTarget(remainder); - } - return { kind: "handle", to: remainder, service }; - } + const servicePrefixed = resolveServicePrefixedTarget({ + trimmed, + lower, + servicePrefixes: SERVICE_PREFIXES, + isChatTarget: (remainderLower) => + CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || + CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) || + CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)) || + remainderLower.startsWith("group:"), + parseTarget: parseBlueBubblesTarget, + }); + if (servicePrefixed) { + return servicePrefixed; } - 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 }; - } + const chatTarget = parseChatTargetPrefixesOrThrow({ + trimmed, + lower, + chatIdPrefixes: CHAT_ID_PREFIXES, + chatGuidPrefixes: CHAT_GUID_PREFIXES, + chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES, + }); + if (chatTarget) { + return chatTarget; } if (lower.startsWith("group:")) { @@ -293,42 +276,25 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget } 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 parseBlueBubblesAllowTarget(remainder); - } + const servicePrefixed = resolveServicePrefixedAllowTarget({ + trimmed, + lower, + servicePrefixes: SERVICE_PREFIXES, + parseAllowTarget: parseBlueBubblesAllowTarget, + }); + if (servicePrefixed) { + return servicePrefixed; } - 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 }; - } - } + const chatTarget = parseChatAllowTargetPrefixes({ + trimmed, + lower, + chatIdPrefixes: CHAT_ID_PREFIXES, + chatGuidPrefixes: CHAT_GUID_PREFIXES, + chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES, + }); + if (chatTarget) { + return chatTarget; } if (lower.startsWith("group:")) { diff --git a/src/imessage/target-parsing-helpers.ts b/src/imessage/target-parsing-helpers.ts new file mode 100644 index 0000000000..2b64c14558 --- /dev/null +++ b/src/imessage/target-parsing-helpers.ts @@ -0,0 +1,132 @@ +export type ServicePrefix = { prefix: string; service: TService }; + +export type ChatTargetPrefixesParams = { + trimmed: string; + lower: string; + chatIdPrefixes: string[]; + chatGuidPrefixes: string[]; + chatIdentifierPrefixes: string[]; +}; + +export type ParsedChatTarget = + | { kind: "chat_id"; chatId: number } + | { kind: "chat_guid"; chatGuid: string } + | { kind: "chat_identifier"; chatIdentifier: string }; + +function stripPrefix(value: string, prefix: string): string { + return value.slice(prefix.length).trim(); +} + +export function resolveServicePrefixedTarget(params: { + trimmed: string; + lower: string; + servicePrefixes: Array>; + isChatTarget: (remainderLower: string) => boolean; + parseTarget: (remainder: string) => TTarget; +}): ({ kind: "handle"; to: string; service: TService } | TTarget) | null { + for (const { prefix, service } of params.servicePrefixes) { + if (!params.lower.startsWith(prefix)) { + continue; + } + const remainder = stripPrefix(params.trimmed, prefix); + if (!remainder) { + throw new Error(`${prefix} target is required`); + } + const remainderLower = remainder.toLowerCase(); + if (params.isChatTarget(remainderLower)) { + return params.parseTarget(remainder); + } + return { kind: "handle", to: remainder, service }; + } + return null; +} + +export function parseChatTargetPrefixesOrThrow( + params: ChatTargetPrefixesParams, +): ParsedChatTarget | null { + for (const prefix of params.chatIdPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.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 params.chatGuidPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (!value) { + throw new Error("chat_guid is required"); + } + return { kind: "chat_guid", chatGuid: value }; + } + } + + for (const prefix of params.chatIdentifierPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (!value) { + throw new Error("chat_identifier is required"); + } + return { kind: "chat_identifier", chatIdentifier: value }; + } + } + + return null; +} + +export function resolveServicePrefixedAllowTarget(params: { + trimmed: string; + lower: string; + servicePrefixes: Array<{ prefix: string }>; + parseAllowTarget: (remainder: string) => TAllowTarget; +}): (TAllowTarget | { kind: "handle"; handle: string }) | null { + for (const { prefix } of params.servicePrefixes) { + if (!params.lower.startsWith(prefix)) { + continue; + } + const remainder = stripPrefix(params.trimmed, prefix); + if (!remainder) { + return { kind: "handle", handle: "" }; + } + return params.parseAllowTarget(remainder); + } + return null; +} + +export function parseChatAllowTargetPrefixes( + params: ChatTargetPrefixesParams, +): ParsedChatTarget | null { + for (const prefix of params.chatIdPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + const chatId = Number.parseInt(value, 10); + if (Number.isFinite(chatId)) { + return { kind: "chat_id", chatId }; + } + } + } + + for (const prefix of params.chatGuidPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (value) { + return { kind: "chat_guid", chatGuid: value }; + } + } + } + + for (const prefix of params.chatIdentifierPrefixes) { + if (params.lower.startsWith(prefix)) { + const value = stripPrefix(params.trimmed, prefix); + if (value) { + return { kind: "chat_identifier", chatIdentifier: value }; + } + } + } + + return null; +} diff --git a/src/imessage/targets.ts b/src/imessage/targets.ts index 3819e1f931..2993594df2 100644 --- a/src/imessage/targets.ts +++ b/src/imessage/targets.ts @@ -1,4 +1,10 @@ import { normalizeE164 } from "../utils.js"; +import { + parseChatAllowTargetPrefixes, + parseChatTargetPrefixesOrThrow, + resolveServicePrefixedAllowTarget, + resolveServicePrefixedTarget, +} from "./target-parsing-helpers.js"; export type IMessageService = "imessage" | "sms" | "auto"; @@ -23,10 +29,6 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: IMessageService }> = [ { 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) { @@ -80,53 +82,29 @@ export function parseIMessageTarget(raw: string): IMessageTarget { } 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 }; - } + const servicePrefixed = resolveServicePrefixedTarget({ + trimmed, + lower, + servicePrefixes: SERVICE_PREFIXES, + isChatTarget: (remainderLower) => + CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || + CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) || + CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)), + parseTarget: parseIMessageTarget, + }); + if (servicePrefixed) { + return servicePrefixed; } - 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 }; - } + const chatTarget = parseChatTargetPrefixesOrThrow({ + trimmed, + lower, + chatIdPrefixes: CHAT_ID_PREFIXES, + chatGuidPrefixes: CHAT_GUID_PREFIXES, + chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES, + }); + if (chatTarget) { + return chatTarget; } return { kind: "handle", to: trimmed, service: "auto" }; @@ -139,42 +117,25 @@ export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget { } 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); - } + const servicePrefixed = resolveServicePrefixedAllowTarget({ + trimmed, + lower, + servicePrefixes: SERVICE_PREFIXES, + parseAllowTarget: parseIMessageAllowTarget, + }); + if (servicePrefixed) { + return servicePrefixed; } - 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 }; - } - } + const chatTarget = parseChatAllowTargetPrefixes({ + trimmed, + lower, + chatIdPrefixes: CHAT_ID_PREFIXES, + chatGuidPrefixes: CHAT_GUID_PREFIXES, + chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES, + }); + if (chatTarget) { + return chatTarget; } return { kind: "handle", handle: normalizeIMessageHandle(trimmed) }; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 8e1b409f87..7b23c66799 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -313,6 +313,12 @@ export { looksLikeIMessageTargetId, normalizeIMessageMessagingTarget, } from "../channels/plugins/normalize/imessage.js"; +export { + parseChatAllowTargetPrefixes, + parseChatTargetPrefixesOrThrow, + resolveServicePrefixedAllowTarget, + resolveServicePrefixedTarget, +} from "../imessage/target-parsing-helpers.js"; // Channel: Slack export {