mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(bluebubbles): tighten chat target handling
This commit is contained in:
@@ -3,17 +3,10 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
|
||||
vi.mock("./accounts.js", () => ({
|
||||
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
|
||||
const config = cfg?.channels?.bluebubbles ?? {};
|
||||
return {
|
||||
accountId: accountId ?? "default",
|
||||
enabled: config.enabled !== false,
|
||||
configured: Boolean(config.serverUrl && config.password),
|
||||
config,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.mock("./accounts.js", async () => {
|
||||
const { createBlueBubblesAccountsMockModule } = await import("./test-harness.js");
|
||||
return createBlueBubblesAccountsMockModule();
|
||||
});
|
||||
|
||||
vi.mock("./reactions.js", () => ({
|
||||
sendBlueBubblesReaction: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import "./test-mocks.js";
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping, setGroupIconBlueBubbles } from "./chat.js";
|
||||
import {
|
||||
addBlueBubblesParticipant,
|
||||
editBlueBubblesMessage,
|
||||
leaveBlueBubblesChat,
|
||||
markBlueBubblesChatRead,
|
||||
removeBlueBubblesParticipant,
|
||||
renameBlueBubblesChat,
|
||||
sendBlueBubblesTyping,
|
||||
setGroupIconBlueBubbles,
|
||||
unsendBlueBubblesMessage,
|
||||
} from "./chat.js";
|
||||
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
||||
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
|
||||
|
||||
@@ -278,6 +288,188 @@ describe("chat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("editBlueBubblesMessage", () => {
|
||||
it("throws when required args are missing", async () => {
|
||||
await expect(editBlueBubblesMessage("", "updated", {})).rejects.toThrow("messageGuid");
|
||||
await expect(editBlueBubblesMessage("message-guid", " ", {})).rejects.toThrow("newText");
|
||||
});
|
||||
|
||||
it("sends edit request with default payload values", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await editBlueBubblesMessage(" message-guid ", " updated text ", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/message/message-guid/edit"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body).toEqual({
|
||||
editedMessage: "updated text",
|
||||
backwardsCompatibilityMessage: "Edited to: updated text",
|
||||
partIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("supports custom part index and backwards compatibility message", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await editBlueBubblesMessage("message-guid", "new text", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
partIndex: 3,
|
||||
backwardsCompatMessage: "custom-backwards-message",
|
||||
});
|
||||
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.partIndex).toBe(3);
|
||||
expect(body.backwardsCompatibilityMessage).toBe("custom-backwards-message");
|
||||
});
|
||||
|
||||
it("throws on non-ok response", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 422,
|
||||
text: () => Promise.resolve("Unprocessable"),
|
||||
});
|
||||
|
||||
await expect(
|
||||
editBlueBubblesMessage("message-guid", "new text", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
}),
|
||||
).rejects.toThrow("edit failed (422): Unprocessable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsendBlueBubblesMessage", () => {
|
||||
it("throws when messageGuid is missing", async () => {
|
||||
await expect(unsendBlueBubblesMessage("", {})).rejects.toThrow("messageGuid");
|
||||
});
|
||||
|
||||
it("sends unsend request with default part index", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await unsendBlueBubblesMessage(" msg-123 ", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/message/msg-123/unsend"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.partIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("uses custom part index", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await unsendBlueBubblesMessage("msg-123", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
partIndex: 2,
|
||||
});
|
||||
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.partIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("group chat mutation actions", () => {
|
||||
it("renames chat", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await renameBlueBubblesChat(" chat-guid ", "New Group Name", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/chat/chat-guid"),
|
||||
expect.objectContaining({ method: "PUT" }),
|
||||
);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.displayName).toBe("New Group Name");
|
||||
});
|
||||
|
||||
it("adds and removes participant using matching endpoint", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await addBlueBubblesParticipant("chat-guid", "+15551234567", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
await removeBlueBubblesParticipant("chat-guid", "+15551234567", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
expect(mockFetch.mock.calls[0][0]).toContain("/api/v1/chat/chat-guid/participant");
|
||||
expect(mockFetch.mock.calls[0][1].method).toBe("POST");
|
||||
expect(mockFetch.mock.calls[1][0]).toContain("/api/v1/chat/chat-guid/participant");
|
||||
expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
|
||||
|
||||
const addBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
const removeBody = JSON.parse(mockFetch.mock.calls[1][1].body);
|
||||
expect(addBody.address).toBe("+15551234567");
|
||||
expect(removeBody.address).toBe("+15551234567");
|
||||
});
|
||||
|
||||
it("leaves chat without JSON body", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(""),
|
||||
});
|
||||
|
||||
await leaveBlueBubblesChat("chat-guid", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/chat/chat-guid/leave"),
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
expect(mockFetch.mock.calls[0][1].body).toBeUndefined();
|
||||
expect(mockFetch.mock.calls[0][1].headers).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setGroupIconBlueBubbles", () => {
|
||||
it("throws when chatGuid is empty", async () => {
|
||||
await expect(
|
||||
|
||||
@@ -26,6 +26,41 @@ function assertPrivateApiEnabled(accountId: string, feature: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePartIndex(partIndex: number | undefined): number {
|
||||
return typeof partIndex === "number" ? partIndex : 0;
|
||||
}
|
||||
|
||||
async function sendPrivateApiJsonRequest(params: {
|
||||
opts: BlueBubblesChatOpts;
|
||||
feature: string;
|
||||
action: string;
|
||||
path: string;
|
||||
method: "POST" | "PUT" | "DELETE";
|
||||
payload?: unknown;
|
||||
}): Promise<void> {
|
||||
const { baseUrl, password, accountId } = resolveAccount(params.opts);
|
||||
assertPrivateApiEnabled(accountId, params.feature);
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
path: params.path,
|
||||
password,
|
||||
});
|
||||
|
||||
const request: RequestInit = { method: params.method };
|
||||
if (params.payload !== undefined) {
|
||||
request.headers = { "Content-Type": "application/json" };
|
||||
request.body = JSON.stringify(params.payload);
|
||||
}
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(url, request, params.opts.timeoutMs);
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`BlueBubbles ${params.action} failed (${res.status}): ${errorText || "unknown"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markBlueBubblesChatRead(
|
||||
chatGuid: string,
|
||||
opts: BlueBubblesChatOpts = {},
|
||||
@@ -97,34 +132,18 @@ export async function editBlueBubblesMessage(
|
||||
throw new Error("BlueBubbles edit requires newText");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "edit");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "edit",
|
||||
action: "edit",
|
||||
method: "POST",
|
||||
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/edit`,
|
||||
password,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
editedMessage: trimmedText,
|
||||
backwardsCompatibilityMessage: opts.backwardsCompatMessage ?? `Edited to: ${trimmedText}`,
|
||||
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
|
||||
};
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
payload: {
|
||||
editedMessage: trimmedText,
|
||||
backwardsCompatibilityMessage: opts.backwardsCompatMessage ?? `Edited to: ${trimmedText}`,
|
||||
partIndex: resolvePartIndex(opts.partIndex),
|
||||
},
|
||||
opts.timeoutMs,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles edit failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,32 +159,14 @@ export async function unsendBlueBubblesMessage(
|
||||
throw new Error("BlueBubbles unsend requires messageGuid");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "unsend");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "unsend",
|
||||
action: "unsend",
|
||||
method: "POST",
|
||||
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/unsend`,
|
||||
password,
|
||||
payload: { partIndex: resolvePartIndex(opts.partIndex) },
|
||||
});
|
||||
|
||||
const payload = {
|
||||
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
|
||||
};
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
opts.timeoutMs,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles unsend failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,28 +182,14 @@ export async function renameBlueBubblesChat(
|
||||
throw new Error("BlueBubbles rename requires chatGuid");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "renameGroup");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "renameGroup",
|
||||
action: "rename",
|
||||
method: "PUT",
|
||||
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}`,
|
||||
password,
|
||||
payload: { displayName },
|
||||
});
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ displayName }),
|
||||
},
|
||||
opts.timeoutMs,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles rename failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,28 +209,14 @@ export async function addBlueBubblesParticipant(
|
||||
throw new Error("BlueBubbles addParticipant requires address");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "addParticipant");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "addParticipant",
|
||||
action: "addParticipant",
|
||||
method: "POST",
|
||||
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
|
||||
password,
|
||||
payload: { address: trimmedAddress },
|
||||
});
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ address: trimmedAddress }),
|
||||
},
|
||||
opts.timeoutMs,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles addParticipant failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,30 +236,14 @@ export async function removeBlueBubblesParticipant(
|
||||
throw new Error("BlueBubbles removeParticipant requires address");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "removeParticipant");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "removeParticipant",
|
||||
action: "removeParticipant",
|
||||
method: "DELETE",
|
||||
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
|
||||
password,
|
||||
payload: { address: trimmedAddress },
|
||||
});
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ address: trimmedAddress }),
|
||||
},
|
||||
opts.timeoutMs,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,20 +258,13 @@ export async function leaveBlueBubblesChat(
|
||||
throw new Error("BlueBubbles leaveChat requires chatGuid");
|
||||
}
|
||||
|
||||
const { baseUrl, password, accountId } = resolveAccount(opts);
|
||||
assertPrivateApiEnabled(accountId, "leaveGroup");
|
||||
const url = buildBlueBubblesApiUrl({
|
||||
baseUrl,
|
||||
await sendPrivateApiJsonRequest({
|
||||
opts,
|
||||
feature: "leaveGroup",
|
||||
action: "leaveChat",
|
||||
method: "POST",
|
||||
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/leave`,
|
||||
password,
|
||||
});
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles leaveChat failed (${res.status}): ${errorText || "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { sendBlueBubblesReaction } from "./reactions.js";
|
||||
|
||||
vi.mock("./accounts.js", () => ({
|
||||
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
|
||||
const config = cfg?.channels?.bluebubbles ?? {};
|
||||
return {
|
||||
accountId: accountId ?? "default",
|
||||
enabled: config.enabled !== false,
|
||||
configured: Boolean(config.serverUrl && config.password),
|
||||
config,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.mock("./accounts.js", async () => {
|
||||
const { createBlueBubblesAccountsMockModule } = await import("./test-harness.js");
|
||||
return createBlueBubblesAccountsMockModule();
|
||||
});
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
|
||||
@@ -78,6 +78,40 @@ function looksLikeRawChatIdentifier(value: string): boolean {
|
||||
return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed);
|
||||
}
|
||||
|
||||
function parseGroupTarget(params: {
|
||||
trimmed: string;
|
||||
lower: string;
|
||||
requireValue: boolean;
|
||||
}): { 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 };
|
||||
}
|
||||
if (params.requireValue) {
|
||||
throw new Error("group target is required");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function normalizeBlueBubblesHandle(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -239,16 +273,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
||||
return chatTarget;
|
||||
}
|
||||
|
||||
if (lower.startsWith("group:")) {
|
||||
const value = stripPrefix(trimmed, "group:");
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(chatId)) {
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
if (!value) {
|
||||
throw new Error("group target is required");
|
||||
}
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
const groupTarget = parseGroupTarget({ trimmed, lower, requireValue: true });
|
||||
if (groupTarget) {
|
||||
return groupTarget;
|
||||
}
|
||||
|
||||
const rawChatGuid = parseRawChatGuid(trimmed);
|
||||
@@ -256,15 +283,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
||||
return { kind: "chat_guid", chatGuid: rawChatGuid };
|
||||
}
|
||||
|
||||
// Handle chat<digits> pattern (e.g., "chat660250192681427962") as chat_identifier
|
||||
// These are BlueBubbles chat identifiers (the third part of a chat GUID), not numeric IDs
|
||||
if (/^chat\d+$/i.test(trimmed)) {
|
||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
||||
}
|
||||
|
||||
// Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc")
|
||||
if (looksLikeRawChatIdentifier(trimmed)) {
|
||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
||||
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
|
||||
if (rawChatIdentifierTarget) {
|
||||
return rawChatIdentifierTarget;
|
||||
}
|
||||
|
||||
return { kind: "handle", to: trimmed, service: "auto" };
|
||||
@@ -298,26 +319,14 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget
|
||||
return chatTarget;
|
||||
}
|
||||
|
||||
if (lower.startsWith("group:")) {
|
||||
const value = stripPrefix(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 };
|
||||
}
|
||||
const groupTarget = parseGroupTarget({ trimmed, lower, requireValue: false });
|
||||
if (groupTarget) {
|
||||
return groupTarget;
|
||||
}
|
||||
|
||||
// Handle chat<digits> pattern (e.g., "chat660250192681427962") as chat_identifier
|
||||
// These are BlueBubbles chat identifiers (the third part of a chat GUID), not numeric IDs
|
||||
if (/^chat\d+$/i.test(trimmed)) {
|
||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
||||
}
|
||||
|
||||
// Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc")
|
||||
if (looksLikeRawChatIdentifier(trimmed)) {
|
||||
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
||||
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
|
||||
if (rawChatIdentifierTarget) {
|
||||
return rawChatIdentifierTarget;
|
||||
}
|
||||
|
||||
return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) };
|
||||
|
||||
Reference in New Issue
Block a user