feat: enhance BlueBubbles messaging targets by adding support for UUID and hex chat identifiers, improving normalization and parsing functions
This commit is contained in:
committed by
Peter Steinberger
parent
199fef2a5e
commit
20bc89d96c
@@ -1336,13 +1336,17 @@ describe("BlueBubbles webhook monitor", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
||||||
|
await params.dispatcherOptions.onReplyStart?.();
|
||||||
|
});
|
||||||
|
|
||||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
||||||
const res = createMockResponse();
|
const res = createMockResponse();
|
||||||
|
|
||||||
await handleBlueBubblesWebhookRequest(req, res);
|
await handleBlueBubblesWebhookRequest(req, res);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
// Should call typing start
|
// Should call typing start when reply flow triggers it.
|
||||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -45,23 +45,22 @@ function logGroupAllowlistHint(params: {
|
|||||||
chatName?: string;
|
chatName?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
}): void {
|
}): void {
|
||||||
const logger = params.runtime.log;
|
const log = params.runtime.log ?? console.log;
|
||||||
if (!logger) return;
|
|
||||||
const nameHint = params.chatName ? ` (group name: ${params.chatName})` : "";
|
const nameHint = params.chatName ? ` (group name: ${params.chatName})` : "";
|
||||||
const accountHint = params.accountId
|
const accountHint = params.accountId
|
||||||
? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)`
|
? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)`
|
||||||
: "";
|
: "";
|
||||||
if (params.entry) {
|
if (params.entry) {
|
||||||
logger(
|
log(
|
||||||
`[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` +
|
`[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` +
|
||||||
`"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`,
|
`"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`,
|
||||||
);
|
);
|
||||||
logger(
|
log(
|
||||||
`[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`,
|
`[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger(
|
log(
|
||||||
`[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` +
|
`[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` +
|
||||||
`channels.bluebubbles.groupPolicy="open" or adding a group id to ` +
|
`channels.bluebubbles.groupPolicy="open" or adding a group id to ` +
|
||||||
`channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`,
|
`channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`,
|
||||||
@@ -1329,18 +1328,6 @@ async function processMessage(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let sentMessage = false;
|
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 {
|
try {
|
||||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ describe("normalizeBlueBubblesMessagingTarget", () => {
|
|||||||
expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
|
expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
|
||||||
expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789");
|
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", () => {
|
describe("looksLikeBlueBubblesTargetId", () => {
|
||||||
@@ -82,6 +91,11 @@ describe("looksLikeBlueBubblesTargetId", () => {
|
|||||||
expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true);
|
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", () => {
|
it("rejects display names", () => {
|
||||||
expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
|
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", () => {
|
it("parses explicit chat_id: prefix", () => {
|
||||||
expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 });
|
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", () => {
|
it("parses explicit chat_id: prefix", () => {
|
||||||
expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
|
expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> =
|
|||||||
{ prefix: "sms:", service: "sms" },
|
{ prefix: "sms:", service: "sms" },
|
||||||
{ prefix: "auto:", service: "auto" },
|
{ 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 {
|
function parseRawChatGuid(value: string): string | null {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -45,6 +48,13 @@ function stripBlueBubblesPrefix(value: string): string {
|
|||||||
return trimmed.slice("bluebubbles:".length).trim();
|
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 {
|
export function normalizeBlueBubblesHandle(raw: string): string {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return "";
|
if (!trimmed) return "";
|
||||||
@@ -113,6 +123,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string):
|
|||||||
}
|
}
|
||||||
// Recognize chat<digits> patterns (e.g., "chat660250192681427962") as chat IDs
|
// Recognize chat<digits> patterns (e.g., "chat660250192681427962") as chat IDs
|
||||||
if (/^chat\d+$/i.test(candidate)) return true;
|
if (/^chat\d+$/i.test(candidate)) return true;
|
||||||
|
if (looksLikeRawChatIdentifier(candidate)) return true;
|
||||||
if (candidate.includes("@")) return true;
|
if (candidate.includes("@")) return true;
|
||||||
const digitsOnly = candidate.replace(/[\s().-]/g, "");
|
const digitsOnly = candidate.replace(/[\s().-]/g, "");
|
||||||
if (/^\+?\d{3,}$/.test(digitsOnly)) return true;
|
if (/^\+?\d{3,}$/.test(digitsOnly)) return true;
|
||||||
@@ -200,6 +211,11 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
|||||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
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" };
|
return { kind: "handle", to: trimmed, service: "auto" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +267,11 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget
|
|||||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
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) };
|
return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user