refactor: remove plugin sdk extension facade smells

This commit is contained in:
Peter Steinberger
2026-03-28 03:11:35 +00:00
parent 8c60e4e9f9
commit 277af32485
3 changed files with 359 additions and 59 deletions

View File

@@ -1,6 +1,241 @@
import {
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
// Narrow plugin-sdk surface for the bundled bluebubbles plugin.
// Keep this list additive and scoped to symbols used under extensions/bluebubbles.
type BlueBubblesService = "imessage" | "sms" | "auto";
type BlueBubblesTarget =
| ParsedChatTarget
| { kind: "handle"; to: string; service: BlueBubblesService };
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: BlueBubblesService }> = [
{ prefix: "imessage:", service: "imessage" },
{ prefix: "sms:", service: "sms" },
{ prefix: "auto:", service: "auto" },
];
const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i;
function parseRawChatGuid(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const parts = trimmed.split(";");
if (parts.length !== 3) {
return null;
}
const service = parts[0]?.trim();
const separator = parts[1]?.trim();
const identifier = parts[2]?.trim();
if (!service || !identifier) {
return null;
}
if (separator !== "+" && separator !== "-") {
return null;
}
return `${service};${separator};${identifier}`;
}
function stripPrefix(value: string, prefix: string): string {
return value.slice(prefix.length).trim();
}
function stripBlueBubblesPrefix(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
if (!trimmed.toLowerCase().startsWith("bluebubbles:")) {
return trimmed;
}
return trimmed.slice("bluebubbles:".length).trim();
}
function looksLikeRawChatIdentifier(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
if (/^chat\d+$/i.test(trimmed)) {
return true;
}
return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed);
}
function parseGroupTarget(params: {
trimmed: string;
lower: string;
}): { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | null {
if (!params.lower.startsWith("group:")) {
return null;
}
const value = stripPrefix(params.trimmed, "group:");
const chatId = Number.parseInt(value, 10);
if (Number.isFinite(chatId)) {
return { kind: "chat_id", chatId };
}
if (value) {
return { kind: "chat_guid", chatGuid: value };
}
throw new Error("group target is required");
}
function parseRawChatIdentifierTarget(
trimmed: string,
): { kind: "chat_identifier"; chatIdentifier: string } | null {
if (/^chat\d+$/i.test(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
if (looksLikeRawChatIdentifier(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
return null;
}
function normalizeBlueBubblesHandle(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return "";
}
const lowered = trimmed.toLowerCase();
if (lowered.startsWith("imessage:")) {
return normalizeBlueBubblesHandle(trimmed.slice(9));
}
if (lowered.startsWith("sms:")) {
return normalizeBlueBubblesHandle(trimmed.slice(4));
}
if (lowered.startsWith("auto:")) {
return normalizeBlueBubblesHandle(trimmed.slice(5));
}
if (trimmed.includes("@")) {
return trimmed.toLowerCase();
}
return trimmed.replace(/\s+/g, "");
}
function extractHandleFromChatGuid(chatGuid: string): string | null {
const parts = chatGuid.split(";");
if (parts.length === 3 && parts[1] === "-") {
const handle = parts[2]?.trim();
if (handle) {
return normalizeBlueBubblesHandle(handle);
}
}
return null;
}
function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
const trimmed = stripBlueBubblesPrefix(raw);
if (!trimmed) {
throw new Error("BlueBubbles target is required");
}
const lower = trimmed.toLowerCase();
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;
}
const chatTarget = parseChatTargetPrefixesOrThrow({
trimmed,
lower,
chatIdPrefixes: CHAT_ID_PREFIXES,
chatGuidPrefixes: CHAT_GUID_PREFIXES,
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
});
if (chatTarget) {
return chatTarget;
}
const groupTarget = parseGroupTarget({ trimmed, lower });
if (groupTarget) {
return groupTarget;
}
const rawChatGuid = parseRawChatGuid(trimmed);
if (rawChatGuid) {
return { kind: "chat_guid", chatGuid: rawChatGuid };
}
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
if (rawChatIdentifierTarget) {
return rawChatIdentifierTarget;
}
return { kind: "handle", to: trimmed, service: "auto" };
}
export function normalizeBlueBubblesAcpConversationId(
conversationId: string,
): { conversationId: string } | null {
const trimmed = conversationId.trim();
if (!trimmed) {
return null;
}
try {
const parsed = parseBlueBubblesTarget(trimmed);
if (parsed.kind === "handle") {
const handle = normalizeBlueBubblesHandle(parsed.to);
return handle ? { conversationId: handle } : null;
}
if (parsed.kind === "chat_id") {
return { conversationId: String(parsed.chatId) };
}
if (parsed.kind === "chat_guid") {
const handle = extractHandleFromChatGuid(parsed.chatGuid);
return {
conversationId: handle || parsed.chatGuid,
};
}
return { conversationId: parsed.chatIdentifier };
} catch {
const handle = normalizeBlueBubblesHandle(trimmed);
return handle ? { conversationId: handle } : null;
}
}
export function matchBlueBubblesAcpConversation(params: {
bindingConversationId: string;
conversationId: string;
}): { conversationId: string; matchPriority: number } | null {
const binding = normalizeBlueBubblesAcpConversationId(params.bindingConversationId);
const conversation = normalizeBlueBubblesAcpConversationId(params.conversationId);
if (!binding || !conversation) {
return null;
}
if (binding.conversationId !== conversation.conversationId) {
return null;
}
return {
conversationId: conversation.conversationId,
matchPriority: 2,
};
}
export function resolveBlueBubblesConversationIdFromTarget(target: string): string | undefined {
return normalizeBlueBubblesAcpConversationId(target)?.conversationId;
}
export { resolveAckReaction } from "../agents/identity.js";
export {
createActionGate,
@@ -74,11 +309,6 @@ export {
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../security/dm-policy-shared.js";
export {
matchBlueBubblesAcpConversation,
normalizeBlueBubblesAcpConversationId,
resolveBlueBubblesConversationIdFromTarget,
} from "../../extensions/bluebubbles/api.js";
export { formatDocsLink } from "../terminal/links.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export { isAllowedParsedChatSender } from "./allow-from.js";

View File

@@ -1,3 +1,12 @@
import {
normalizeIMessageHandle,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
export type { ChannelPlugin } from "./channel-plugin-common.js";
export {
DEFAULT_ACCOUNT_ID,
@@ -13,14 +22,113 @@ export {
} from "./channel-config-helpers.js";
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export {
matchIMessageAcpConversation,
normalizeIMessageAcpConversationId,
resolveIMessageConversationIdFromTarget,
} from "../../extensions/imessage/api.js";
export {
normalizeIMessageHandle,
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./imessage-targets.js";
type IMessageService = "imessage" | "sms" | "auto";
type IMessageTarget = ParsedChatTarget | { kind: "handle"; to: string; service: IMessageService };
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 startsWithAnyPrefix(value: string, prefixes: readonly string[]): boolean {
return prefixes.some((prefix) => value.startsWith(prefix));
}
function parseIMessageTarget(raw: string): IMessageTarget {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("iMessage target is required");
}
const lower = trimmed.toLowerCase();
const servicePrefixed = resolveServicePrefixedTarget({
trimmed,
lower,
servicePrefixes: SERVICE_PREFIXES,
isChatTarget: (remainderLower) =>
startsWithAnyPrefix(remainderLower, [
...CHAT_ID_PREFIXES,
...CHAT_GUID_PREFIXES,
...CHAT_IDENTIFIER_PREFIXES,
]),
parseTarget: parseIMessageTarget,
});
if (servicePrefixed) {
return servicePrefixed;
}
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" };
}
export function normalizeIMessageAcpConversationId(
conversationId: string,
): { conversationId: string } | null {
const trimmed = conversationId.trim();
if (!trimmed) {
return null;
}
try {
const parsed = parseIMessageTarget(trimmed);
if (parsed.kind === "handle") {
const handle = normalizeIMessageHandle(parsed.to);
return handle ? { conversationId: handle } : null;
}
if (parsed.kind === "chat_id") {
return { conversationId: String(parsed.chatId) };
}
if (parsed.kind === "chat_guid") {
return { conversationId: parsed.chatGuid };
}
return { conversationId: parsed.chatIdentifier };
} catch {
const handle = normalizeIMessageHandle(trimmed);
return handle ? { conversationId: handle } : null;
}
}
export function matchIMessageAcpConversation(params: {
bindingConversationId: string;
conversationId: string;
}): { conversationId: string; matchPriority: number } | null {
const binding = normalizeIMessageAcpConversationId(params.bindingConversationId);
const conversation = normalizeIMessageAcpConversationId(params.conversationId);
if (!binding || !conversation) {
return null;
}
if (binding.conversationId !== conversation.conversationId) {
return null;
}
return {
conversationId: conversation.conversationId,
matchPriority: 2,
};
}
export function resolveIMessageConversationIdFromTarget(target: string): string | undefined {
return normalizeIMessageAcpConversationId(target)?.conversationId;
}