mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-02-01 03:47:45 +01:00
feat: enhance BlueBubbles channel integration with new messaging target normalization and typing indicator improvements
This commit is contained in:
committed by
Peter Steinberger
parent
61907ddf3e
commit
a5d89e6eb1
@@ -26,7 +26,11 @@ import { BlueBubblesConfigSchema } from "./config-schema.js";
|
||||
import { probeBlueBubbles } from "./probe.js";
|
||||
import { sendMessageBlueBubbles } from "./send.js";
|
||||
import { sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import { normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import {
|
||||
looksLikeBlueBubblesTargetId,
|
||||
normalizeBlueBubblesHandle,
|
||||
normalizeBlueBubblesMessagingTarget,
|
||||
} from "./targets.js";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
|
||||
import { blueBubblesOnboardingAdapter } from "./onboarding.js";
|
||||
@@ -153,6 +157,13 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
];
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeBlueBubblesMessagingTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeBlueBubblesTargetId,
|
||||
hint: "<handle|chat_guid:GUID|chat_id:ID|chat_identifier:ID>",
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
|
||||
@@ -1001,6 +1001,7 @@ async function processMessage(
|
||||
CommandAuthorized: commandAuthorized,
|
||||
};
|
||||
|
||||
let sentMessage = false;
|
||||
if (chatGuidForActions && baseUrl && password) {
|
||||
logVerbose(core, runtime, `typing start (pre-dispatch) chatGuid=${chatGuidForActions}`);
|
||||
try {
|
||||
@@ -1031,6 +1032,7 @@ async function processMessage(
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
}
|
||||
},
|
||||
@@ -1048,15 +1050,7 @@ async function processMessage(
|
||||
}
|
||||
},
|
||||
onIdle: () => {
|
||||
if (!chatGuidForActions) return;
|
||||
if (!baseUrl || !password) return;
|
||||
logVerbose(core, runtime, `typing stop chatGuid=${chatGuidForActions}`);
|
||||
void sendBlueBubblesTyping(chatGuidForActions, false, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
}).catch((err) => {
|
||||
runtime.error?.(`[bluebubbles] typing stop failed: ${String(err)}`);
|
||||
});
|
||||
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
|
||||
@@ -1070,14 +1064,8 @@ async function processMessage(
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (chatGuidForActions && baseUrl && password) {
|
||||
logVerbose(core, runtime, `typing stop (finalize) chatGuid=${chatGuidForActions}`);
|
||||
void sendBlueBubblesTyping(chatGuidForActions, false, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
}).catch((err) => {
|
||||
runtime.error?.(`[bluebubbles] typing stop failed: ${String(err)}`);
|
||||
});
|
||||
if (chatGuidForActions && baseUrl && password && !sentMessage) {
|
||||
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
extensions/bluebubbles/src/targets.test.ts
Normal file
40
extensions/bluebubbles/src/targets.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
looksLikeBlueBubblesTargetId,
|
||||
normalizeBlueBubblesMessagingTarget,
|
||||
} from "./targets.js";
|
||||
|
||||
describe("normalizeBlueBubblesMessagingTarget", () => {
|
||||
it("normalizes chat_guid targets", () => {
|
||||
expect(normalizeBlueBubblesMessagingTarget("chat_guid:ABC-123")).toBe("chat_guid:ABC-123");
|
||||
});
|
||||
|
||||
it("normalizes group numeric targets to chat_id", () => {
|
||||
expect(normalizeBlueBubblesMessagingTarget("group:123")).toBe("chat_id:123");
|
||||
});
|
||||
|
||||
it("strips provider prefix and normalizes handles", () => {
|
||||
expect(
|
||||
normalizeBlueBubblesMessagingTarget("bluebubbles:imessage:User@Example.com"),
|
||||
).toBe("imessage:user@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("looksLikeBlueBubblesTargetId", () => {
|
||||
it("accepts chat targets", () => {
|
||||
expect(looksLikeBlueBubblesTargetId("chat_guid:ABC-123")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts email handles", () => {
|
||||
expect(looksLikeBlueBubblesTargetId("user@example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts phone numbers with punctuation", () => {
|
||||
expect(looksLikeBlueBubblesTargetId("+1 (555) 123-4567")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects display names", () => {
|
||||
expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,13 @@ 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();
|
||||
}
|
||||
|
||||
export function normalizeBlueBubblesHandle(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return "";
|
||||
@@ -36,6 +43,55 @@ export function normalizeBlueBubblesHandle(raw: string): string {
|
||||
return trimmed.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
export function normalizeBlueBubblesMessagingTarget(raw: string): string | undefined {
|
||||
let trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
trimmed = stripBlueBubblesPrefix(trimmed);
|
||||
if (!trimmed) return undefined;
|
||||
try {
|
||||
const parsed = parseBlueBubblesTarget(trimmed);
|
||||
if (parsed.kind === "chat_id") return `chat_id:${parsed.chatId}`;
|
||||
if (parsed.kind === "chat_guid") return `chat_guid:${parsed.chatGuid}`;
|
||||
if (parsed.kind === "chat_identifier") return `chat_identifier:${parsed.chatIdentifier}`;
|
||||
const handle = normalizeBlueBubblesHandle(parsed.to);
|
||||
if (!handle) return undefined;
|
||||
return parsed.service === "auto" ? handle : `${parsed.service}:${handle}`;
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
const candidate = stripBlueBubblesPrefix(trimmed);
|
||||
if (!candidate) return false;
|
||||
const lowered = candidate.toLowerCase();
|
||||
if (/^(imessage|sms|auto):/.test(lowered)) return true;
|
||||
if (
|
||||
/^(chat_id|chatid|chat|chat_guid|chatguid|guid|chat_identifier|chatidentifier|chatident|group):/.test(
|
||||
lowered,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (candidate.includes("@")) return true;
|
||||
const digitsOnly = candidate.replace(/[\s().-]/g, "");
|
||||
if (/^\+?\d{3,}$/.test(digitsOnly)) return true;
|
||||
if (normalized) {
|
||||
const normalizedTrimmed = normalized.trim();
|
||||
if (!normalizedTrimmed) return false;
|
||||
const normalizedLower = normalizedTrimmed.toLowerCase();
|
||||
if (
|
||||
/^(imessage|sms|auto):/.test(normalizedLower) ||
|
||||
/^(chat_id|chat_guid|chat_identifier):/.test(normalizedLower)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) throw new Error("BlueBubbles target is required");
|
||||
|
||||
Reference in New Issue
Block a user