feat: enhance BlueBubbles messaging targets by adding support for UUID and hex chat identifiers, improving normalization and parsing functions

This commit is contained in:
Tyler Yust
2026-01-20 01:14:40 -08:00
committed by Peter Steinberger
parent 199fef2a5e
commit 20bc89d96c
4 changed files with 66 additions and 18 deletions

View File

@@ -1336,13 +1336,17 @@ describe("BlueBubbles webhook monitor", () => {
},
};
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
await params.dispatcherOptions.onReplyStart?.();
});
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
await new Promise((resolve) => setTimeout(resolve, 50));
// Should call typing start
// Should call typing start when reply flow triggers it.
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
expect.any(String),
true,

View File

@@ -45,23 +45,22 @@ function logGroupAllowlistHint(params: {
chatName?: string;
accountId?: string;
}): void {
const logger = params.runtime.log;
if (!logger) return;
const log = params.runtime.log ?? console.log;
const nameHint = params.chatName ? ` (group name: ${params.chatName})` : "";
const accountHint = params.accountId
? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)`
: "";
if (params.entry) {
logger(
log(
`[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` +
`"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`,
);
logger(
log(
`[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`,
);
return;
}
logger(
log(
`[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` +
`channels.bluebubbles.groupPolicy="open" or adding a group id to ` +
`channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`,
@@ -1329,18 +1328,6 @@ async function processMessage(
};
let sentMessage = false;
if (chatGuidForActions && baseUrl && password) {
logVerbose(core, runtime, `typing start (pre-dispatch) chatGuid=${chatGuidForActions}`);
try {
await sendBlueBubblesTyping(chatGuidForActions, true, {
cfg: config,
accountId: account.accountId,
});
} catch (err) {
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
}
}
try {
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,

View File

@@ -57,6 +57,15 @@ describe("normalizeBlueBubblesMessagingTarget", () => {
expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789");
});
it("normalizes UUID/hex chat identifiers", () => {
expect(normalizeBlueBubblesMessagingTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(
"chat_identifier:8b9c1a10536d4d86a336ea03ab7151cc",
);
expect(normalizeBlueBubblesMessagingTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(
"chat_identifier:1C2D3E4F-1234-5678-9ABC-DEF012345678",
);
});
});
describe("looksLikeBlueBubblesTargetId", () => {
@@ -82,6 +91,11 @@ describe("looksLikeBlueBubblesTargetId", () => {
expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true);
});
it("accepts UUID/hex chat identifiers", () => {
expect(looksLikeBlueBubblesTargetId("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(true);
expect(looksLikeBlueBubblesTargetId("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(true);
});
it("rejects display names", () => {
expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
});
@@ -103,6 +117,17 @@ describe("parseBlueBubblesTarget", () => {
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 });
});
@@ -135,6 +160,17 @@ describe("parseBlueBubblesAllowTarget", () => {
});
});
it("parses UUID/hex chat identifiers as chat_identifier", () => {
expect(parseBlueBubblesAllowTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
kind: "chat_identifier",
chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
});
expect(parseBlueBubblesAllowTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
kind: "chat_identifier",
chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
});
});
it("parses explicit chat_id: prefix", () => {
expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
});

View File

@@ -20,6 +20,9 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> =
{ 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();
@@ -45,6 +48,13 @@ function stripBlueBubblesPrefix(value: string): string {
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);
}
export function normalizeBlueBubblesHandle(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) return "";
@@ -113,6 +123,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string):
}
// Recognize chat<digits> patterns (e.g., "chat660250192681427962") as chat IDs
if (/^chat\d+$/i.test(candidate)) return true;
if (looksLikeRawChatIdentifier(candidate)) return true;
if (candidate.includes("@")) return true;
const digitsOnly = candidate.replace(/[\s().-]/g, "");
if (/^\+?\d{3,}$/.test(digitsOnly)) return true;
@@ -200,6 +211,11 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
// Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc")
if (looksLikeRawChatIdentifier(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
return { kind: "handle", to: trimmed, service: "auto" };
}
@@ -251,6 +267,11 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
// Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc")
if (looksLikeRawChatIdentifier(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) };
}