mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 03:04:29 -04:00
chore: Run pnpm format:fix.
This commit is contained in:
@@ -300,9 +300,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
const resolvedChatGuid = await resolveChatGuid();
|
||||
const base64Buffer = readStringParam(params, "buffer");
|
||||
const filename =
|
||||
readStringParam(params, "filename") ??
|
||||
readStringParam(params, "name") ??
|
||||
"icon.png";
|
||||
readStringParam(params, "filename") ?? readStringParam(params, "name") ?? "icon.png";
|
||||
const contentType =
|
||||
readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");
|
||||
|
||||
|
||||
@@ -39,8 +39,10 @@ function ensureExtension(filename: string, extension: string, fallbackBase: stri
|
||||
function resolveVoiceInfo(filename: string, contentType?: string) {
|
||||
const normalizedType = contentType?.trim().toLowerCase();
|
||||
const extension = path.extname(filename).toLowerCase();
|
||||
const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
|
||||
const isCaf = extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
|
||||
const isMp3 =
|
||||
extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
|
||||
const isCaf =
|
||||
extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
|
||||
const isAudio = isMp3 || isCaf || Boolean(normalizedType?.startsWith("audio/"));
|
||||
return { isAudio, isMp3, isCaf };
|
||||
}
|
||||
@@ -110,7 +112,10 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget {
|
||||
function extractMessageId(payload: unknown): string {
|
||||
if (!payload || typeof payload !== "object") return "unknown";
|
||||
const record = payload as Record<string, unknown>;
|
||||
const data = record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
|
||||
const data =
|
||||
record.data && typeof record.data === "object"
|
||||
? (record.data as Record<string, unknown>)
|
||||
: null;
|
||||
const candidates = [
|
||||
record.messageId,
|
||||
record.guid,
|
||||
@@ -205,9 +210,7 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
const addFile = (name: string, fileBuffer: Uint8Array, fileName: string, mimeType?: string) => {
|
||||
parts.push(encoder.encode(`--${boundary}\r\n`));
|
||||
parts.push(
|
||||
encoder.encode(
|
||||
`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`,
|
||||
),
|
||||
encoder.encode(`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`),
|
||||
);
|
||||
parts.push(encoder.encode(`Content-Type: ${mimeType ?? "application/octet-stream"}\r\n\r\n`));
|
||||
parts.push(fileBuffer);
|
||||
@@ -229,10 +232,7 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
const trimmedReplyTo = replyToMessageGuid?.trim();
|
||||
if (trimmedReplyTo) {
|
||||
addField("selectedMessageGuid", trimmedReplyTo);
|
||||
addField(
|
||||
"partIndex",
|
||||
typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0",
|
||||
);
|
||||
addField("partIndex", typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0");
|
||||
}
|
||||
|
||||
// Add optional caption
|
||||
@@ -268,7 +268,9 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
throw new Error(`BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`);
|
||||
throw new Error(
|
||||
`BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const responseBody = await res.text();
|
||||
|
||||
@@ -106,10 +106,9 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
(resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ??
|
||||
[]).map(
|
||||
(entry) => String(entry),
|
||||
),
|
||||
(
|
||||
resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? []
|
||||
).map((entry) => String(entry)),
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
|
||||
@@ -249,7 +249,9 @@ export async function removeBlueBubblesParticipant(
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`);
|
||||
throw new Error(
|
||||
`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,11 +272,7 @@ export async function leaveBlueBubblesChat(
|
||||
password,
|
||||
});
|
||||
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{ method: "POST" },
|
||||
opts.timeoutMs,
|
||||
);
|
||||
const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
@@ -313,9 +311,7 @@ export async function setGroupIconBlueBubbles(
|
||||
// Add file field named "icon" as per API spec
|
||||
parts.push(encoder.encode(`--${boundary}\r\n`));
|
||||
parts.push(
|
||||
encoder.encode(
|
||||
`Content-Disposition: form-data; name="icon"; filename="${filename}"\r\n`,
|
||||
),
|
||||
encoder.encode(`Content-Disposition: form-data; name="icon"; filename="${filename}"\r\n`),
|
||||
);
|
||||
parts.push(
|
||||
encoder.encode(`Content-Type: ${opts.contentType ?? "application/octet-stream"}\r\n\r\n`),
|
||||
|
||||
@@ -78,88 +78,131 @@ function createMockRuntime(): PluginRuntime {
|
||||
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
},
|
||||
system: {
|
||||
enqueueSystemEvent: mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
||||
enqueueSystemEvent:
|
||||
mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
||||
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
|
||||
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
|
||||
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
||||
isVoiceCompatibleAudio: vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
||||
isVoiceCompatibleAudio:
|
||||
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
||||
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
|
||||
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
||||
},
|
||||
tools: {
|
||||
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
|
||||
createMemorySearchTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
||||
createMemorySearchTool:
|
||||
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
||||
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
chunkMarkdownText: mockChunkMarkdownText as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownText"],
|
||||
chunkMarkdownText:
|
||||
mockChunkMarkdownText as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownText"],
|
||||
chunkText: vi.fn() as unknown as PluginRuntime["channel"]["text"]["chunkText"],
|
||||
resolveTextChunkLimit: vi.fn(() => 4000) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
||||
hasControlCommand: mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
||||
resolveMarkdownTableMode: vi.fn(() => "code") as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
||||
convertMarkdownTables: vi.fn((text: string) => text) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
||||
resolveTextChunkLimit: vi.fn(
|
||||
() => 4000,
|
||||
) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
||||
hasControlCommand:
|
||||
mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
||||
resolveMarkdownTableMode: vi.fn(
|
||||
() => "code",
|
||||
) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
||||
convertMarkdownTables: vi.fn(
|
||||
(text: string) => text,
|
||||
) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
createReplyDispatcherWithTyping: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
|
||||
resolveEffectiveMessagesConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
||||
resolveHumanDelayConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
||||
dispatchReplyFromConfig: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
finalizeInboundContext: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
formatAgentEnvelope: mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
||||
formatInboundEnvelope: vi.fn() as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
||||
resolveEnvelopeFormatOptions: mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
||||
dispatchReplyWithBufferedBlockDispatcher:
|
||||
mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
createReplyDispatcherWithTyping:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
|
||||
resolveEffectiveMessagesConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
||||
resolveHumanDelayConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
||||
dispatchReplyFromConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
finalizeInboundContext:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
formatAgentEnvelope:
|
||||
mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
||||
formatInboundEnvelope:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
||||
resolveEnvelopeFormatOptions:
|
||||
mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute: mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
||||
resolveAgentRoute:
|
||||
mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
||||
},
|
||||
pairing: {
|
||||
buildPairingReply: mockBuildPairingReply as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
|
||||
readAllowFromStore: mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
||||
upsertPairingRequest: mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
||||
buildPairingReply:
|
||||
mockBuildPairingReply as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
|
||||
readAllowFromStore:
|
||||
mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
||||
upsertPairingRequest:
|
||||
mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia: vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer: mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
fetchRemoteMedia:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer:
|
||||
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
},
|
||||
session: {
|
||||
resolveStorePath: mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt: mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordInboundSession: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
recordSessionMetaFromInbound: vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
updateLastRoute: vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
resolveStorePath:
|
||||
mockResolveStorePath as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt:
|
||||
mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordInboundSession:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
recordSessionMetaFromInbound:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
updateLastRoute:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes: mockBuildMentionRegexes as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
|
||||
matchesMentionPatterns: mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
||||
buildMentionRegexes:
|
||||
mockBuildMentionRegexes as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
|
||||
matchesMentionPatterns:
|
||||
mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
resolveGroupPolicy:
|
||||
mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention:
|
||||
mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
},
|
||||
debounce: {
|
||||
// Create a pass-through debouncer that immediately calls onFlush
|
||||
createInboundDebouncer: vi.fn((params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||
enqueue: async (item: unknown) => {
|
||||
await params.onFlush([item]);
|
||||
},
|
||||
flushKey: vi.fn(),
|
||||
})) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||
resolveInboundDebounceMs: vi.fn(() => 0) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||
createInboundDebouncer: vi.fn(
|
||||
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||
enqueue: async (item: unknown) => {
|
||||
await params.onFlush([item]);
|
||||
},
|
||||
flushKey: vi.fn(),
|
||||
}),
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||
resolveInboundDebounceMs: vi.fn(
|
||||
() => 0,
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
shouldComputeCommandAuthorized: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
shouldHandleTextCommands: vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
resolveCommandAuthorizedFromAuthorizers:
|
||||
mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
shouldComputeCommandAuthorized:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
shouldHandleTextCommands:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
},
|
||||
discord: {} as PluginRuntime["channel"]["discord"],
|
||||
slack: {} as PluginRuntime["channel"]["slack"],
|
||||
@@ -169,7 +212,9 @@ function createMockRuntime(): PluginRuntime {
|
||||
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: vi.fn(() => false) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
|
||||
shouldLogVerbose: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
|
||||
getChildLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -178,12 +223,16 @@ function createMockRuntime(): PluginRuntime {
|
||||
})) as unknown as PluginRuntime["logging"]["getChildLogger"],
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: vi.fn(() => "/tmp/openclaw") as unknown as PluginRuntime["state"]["resolveStateDir"],
|
||||
resolveStateDir: vi.fn(
|
||||
() => "/tmp/openclaw",
|
||||
) as unknown as PluginRuntime["state"]["resolveStateDir"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createMockAccount(overrides: Partial<ResolvedBlueBubblesAccount["config"]> = {}): ResolvedBlueBubblesAccount {
|
||||
function createMockAccount(
|
||||
overrides: Partial<ResolvedBlueBubblesAccount["config"]> = {},
|
||||
): ResolvedBlueBubblesAccount {
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
@@ -361,7 +410,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
guid: "msg-1",
|
||||
},
|
||||
});
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
||||
remoteAddress: "192.168.1.100",
|
||||
};
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
@@ -399,7 +450,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
},
|
||||
{ "x-password": "secret-token" },
|
||||
);
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
||||
remoteAddress: "192.168.1.100",
|
||||
};
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
@@ -432,7 +485,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
guid: "msg-1",
|
||||
},
|
||||
});
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "192.168.1.100" };
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
||||
remoteAddress: "192.168.1.100",
|
||||
};
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
@@ -466,7 +521,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
},
|
||||
});
|
||||
// Localhost address
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "127.0.0.1" };
|
||||
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
||||
remoteAddress: "127.0.0.1",
|
||||
};
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
@@ -1161,7 +1218,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
// Use a timing-aware debouncer test double that respects debounceMs/buildKey/shouldDebounce.
|
||||
core.channel.debounce.createInboundDebouncer = vi.fn((params: any) => {
|
||||
type Item = any;
|
||||
const buckets = new Map<string, { items: Item[]; timer: ReturnType<typeof setTimeout> | null }>();
|
||||
const buckets = new Map<
|
||||
string,
|
||||
{ items: Item[]; timer: ReturnType<typeof setTimeout> | null }
|
||||
>();
|
||||
|
||||
const flush = async (key: string) => {
|
||||
const bucket = buckets.get(key);
|
||||
@@ -2231,9 +2291,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("throws when numeric short ID is missing and requireKnownShortId is set", () => {
|
||||
expect(() =>
|
||||
resolveBlueBubblesMessageId("999", { requireKnownShortId: true }),
|
||||
).toThrow(/short message id/i);
|
||||
expect(() => resolveBlueBubblesMessageId("999", { requireKnownShortId: true })).toThrow(
|
||||
/short message id/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
||||
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender, normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import {
|
||||
formatBlueBubblesChatTarget,
|
||||
isAllowedBlueBubblesSender,
|
||||
normalizeBlueBubblesHandle,
|
||||
} from "./targets.js";
|
||||
import { sendBlueBubblesMedia } from "./media-send.js";
|
||||
import type { BlueBubblesAccountConfig, BlueBubblesAttachment } from "./types.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
@@ -194,7 +198,12 @@ function resolveReplyContextFromCache(params: {
|
||||
|
||||
// Avoid cross-chat collisions if we have identifiers.
|
||||
if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) return null;
|
||||
if (!chatGuid && chatIdentifier && cachedChatIdentifier && chatIdentifier !== cachedChatIdentifier) {
|
||||
if (
|
||||
!chatGuid &&
|
||||
chatIdentifier &&
|
||||
cachedChatIdentifier &&
|
||||
chatIdentifier !== cachedChatIdentifier
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!chatGuid && !chatIdentifier && chatId && cachedChatId && chatId !== cachedChatId) {
|
||||
@@ -206,7 +215,11 @@ function resolveReplyContextFromCache(params: {
|
||||
|
||||
type BlueBubblesCoreRuntime = ReturnType<typeof getBlueBubblesRuntime>;
|
||||
|
||||
function logVerbose(core: BlueBubblesCoreRuntime, runtime: BlueBubblesRuntimeEnv, message: string): void {
|
||||
function logVerbose(
|
||||
core: BlueBubblesCoreRuntime,
|
||||
runtime: BlueBubblesRuntimeEnv,
|
||||
message: string,
|
||||
): void {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
runtime.log?.(`[bluebubbles] ${message}`);
|
||||
}
|
||||
@@ -284,7 +297,7 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized
|
||||
// Combine text from all entries, filtering out duplicates and empty strings
|
||||
const seenTexts = new Set<string>();
|
||||
const textParts: string[] = [];
|
||||
|
||||
|
||||
for (const entry of entries) {
|
||||
const text = entry.message.text.trim();
|
||||
if (!text) continue;
|
||||
@@ -398,10 +411,10 @@ function getOrCreateDebouncer(target: WebhookTarget) {
|
||||
},
|
||||
onFlush: async (entries) => {
|
||||
if (entries.length === 0) return;
|
||||
|
||||
|
||||
// Use target from first entry (all entries have same target due to key structure)
|
||||
const flushTarget = entries[0].target;
|
||||
|
||||
|
||||
if (entries.length === 1) {
|
||||
// Single message - process normally
|
||||
await processMessage(entries[0].message, flushTarget);
|
||||
@@ -410,7 +423,7 @@ function getOrCreateDebouncer(target: WebhookTarget) {
|
||||
|
||||
// Multiple messages - combine and process
|
||||
const combined = combineDebounceEntries(entries);
|
||||
|
||||
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
const count = entries.length;
|
||||
const preview = combined.text.slice(0, 50);
|
||||
@@ -418,7 +431,7 @@ function getOrCreateDebouncer(target: WebhookTarget) {
|
||||
`[bluebubbles] coalesced ${count} messages: "${preview}${combined.text.length > 50 ? "..." : ""}"`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
await processMessage(combined, flushTarget);
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -578,10 +591,7 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
|
||||
}
|
||||
|
||||
// Returns inline reply tag like "[[reply_to:4]]" for prepending to message body
|
||||
function formatReplyTag(message: {
|
||||
replyToId?: string;
|
||||
replyToShortId?: string;
|
||||
}): string | null {
|
||||
function formatReplyTag(message: { replyToId?: string; replyToShortId?: string }): string | null {
|
||||
// Prefer short ID
|
||||
const rawId = message.replyToShortId || message.replyToId;
|
||||
if (!rawId) return null;
|
||||
@@ -614,7 +624,8 @@ function extractReplyMetadata(message: Record<string, unknown>): {
|
||||
message["associatedMessage"] ??
|
||||
message["reply"];
|
||||
const replyRecord = asRecord(replyRaw);
|
||||
const replyHandle = asRecord(replyRecord?.["handle"]) ?? asRecord(replyRecord?.["sender"]) ?? null;
|
||||
const replyHandle =
|
||||
asRecord(replyRecord?.["handle"]) ?? asRecord(replyRecord?.["sender"]) ?? null;
|
||||
const replySenderRaw =
|
||||
readString(replyHandle, "address") ??
|
||||
readString(replyHandle, "handle") ??
|
||||
@@ -742,9 +753,7 @@ function formatGroupMembers(params: {
|
||||
ordered.push(params.fallback);
|
||||
}
|
||||
if (ordered.length === 0) return undefined;
|
||||
return ordered
|
||||
.map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id))
|
||||
.join(", ");
|
||||
return ordered.map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id)).join(", ");
|
||||
}
|
||||
|
||||
function resolveGroupFlagFromChatGuid(chatGuid?: string | null): boolean | undefined {
|
||||
@@ -992,7 +1001,9 @@ function extractMessagePayload(payload: Record<string, unknown>): Record<string,
|
||||
return message;
|
||||
}
|
||||
|
||||
function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWebhookMessage | null {
|
||||
function normalizeWebhookMessage(
|
||||
payload: Record<string, unknown>,
|
||||
): NormalizedWebhookMessage | null {
|
||||
const message = extractMessagePayload(payload);
|
||||
if (!message) return null;
|
||||
|
||||
@@ -1004,8 +1015,7 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
|
||||
|
||||
const handleValue = message.handle ?? message.sender;
|
||||
const handle =
|
||||
asRecord(handleValue) ??
|
||||
(typeof handleValue === "string" ? { address: handleValue } : null);
|
||||
asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
|
||||
const senderId =
|
||||
readString(handle, "address") ??
|
||||
readString(handle, "handle") ??
|
||||
@@ -1080,7 +1090,7 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
|
||||
const isGroup =
|
||||
typeof groupFromChatGuid === "boolean"
|
||||
? groupFromChatGuid
|
||||
: explicitIsGroup ?? (participantsCount > 2 ? true : false);
|
||||
: (explicitIsGroup ?? (participantsCount > 2 ? true : false));
|
||||
|
||||
const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
|
||||
const messageId =
|
||||
@@ -1149,7 +1159,9 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedWebhookReaction | null {
|
||||
function normalizeWebhookReaction(
|
||||
payload: Record<string, unknown>,
|
||||
): NormalizedWebhookReaction | null {
|
||||
const message = extractMessagePayload(payload);
|
||||
if (!message) return null;
|
||||
|
||||
@@ -1173,8 +1185,7 @@ function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedW
|
||||
|
||||
const handleValue = message.handle ?? message.sender;
|
||||
const handle =
|
||||
asRecord(handleValue) ??
|
||||
(typeof handleValue === "string" ? { address: handleValue } : null);
|
||||
asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
|
||||
const senderId =
|
||||
readString(handle, "address") ??
|
||||
readString(handle, "handle") ??
|
||||
@@ -1247,7 +1258,7 @@ function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedW
|
||||
const isGroup =
|
||||
typeof groupFromChatGuid === "boolean"
|
||||
? groupFromChatGuid
|
||||
: explicitIsGroup ?? (participantsCount > 2 ? true : false);
|
||||
: (explicitIsGroup ?? (participantsCount > 2 ? true : false));
|
||||
|
||||
const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
|
||||
const timestampRaw =
|
||||
@@ -1364,8 +1375,7 @@ export async function handleBlueBubblesWebhookRequest(
|
||||
req.headers["x-password"] ??
|
||||
req.headers["x-bluebubbles-guid"] ??
|
||||
req.headers["authorization"];
|
||||
const guid =
|
||||
(Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
|
||||
const guid = (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? "";
|
||||
if (guid && guid.trim() === token) return true;
|
||||
const remote = req.socket?.remoteAddress ?? "";
|
||||
if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") {
|
||||
@@ -1630,7 +1640,7 @@ async function processMessage(
|
||||
const chatGuid = message.chatGuid ?? undefined;
|
||||
const chatIdentifier = message.chatIdentifier ?? undefined;
|
||||
const peerId = isGroup
|
||||
? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
|
||||
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
|
||||
: message.senderId;
|
||||
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
@@ -1705,11 +1715,7 @@ async function processMessage(
|
||||
|
||||
// Allow control commands to bypass mention gating when authorized (parity with iMessage)
|
||||
const shouldBypassMention =
|
||||
isGroup &&
|
||||
requireMention &&
|
||||
!wasMentioned &&
|
||||
commandAuthorized &&
|
||||
hasControlCmd;
|
||||
isGroup && requireMention && !wasMentioned && commandAuthorized && hasControlCmd;
|
||||
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
|
||||
|
||||
// Skip group messages that require mention but weren't mentioned
|
||||
@@ -1872,16 +1878,16 @@ async function processMessage(
|
||||
const shouldAckReaction = () =>
|
||||
Boolean(
|
||||
ackReactionValue &&
|
||||
core.channel.reactions.shouldAckReaction({
|
||||
scope: ackReactionScope,
|
||||
isDirect: !isGroup,
|
||||
isGroup,
|
||||
isMentionableGroup: isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
core.channel.reactions.shouldAckReaction({
|
||||
scope: ackReactionScope,
|
||||
isDirect: !isGroup,
|
||||
isGroup,
|
||||
isMentionableGroup: isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
);
|
||||
const ackMessageId = message.messageId?.trim() || "";
|
||||
const ackReactionPromise =
|
||||
@@ -2000,7 +2006,8 @@ async function processMessage(
|
||||
cfg: config,
|
||||
dispatcherOptions: {
|
||||
deliver: async (payload) => {
|
||||
const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
|
||||
const rawReplyToId =
|
||||
typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
|
||||
// Resolve short ID (e.g., "5") to full UUID
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
@@ -2210,7 +2217,7 @@ async function processReaction(
|
||||
const chatGuid = reaction.chatGuid ?? undefined;
|
||||
const chatIdentifier = reaction.chatIdentifier ?? undefined;
|
||||
const peerId = reaction.isGroup
|
||||
? chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group")
|
||||
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
|
||||
: reaction.senderId;
|
||||
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
|
||||
@@ -108,11 +108,7 @@ export async function probeBlueBubbles(params: {
|
||||
if (!password) return { ok: false, error: "password not configured" };
|
||||
const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/ping", password });
|
||||
try {
|
||||
const res = await blueBubblesFetchWithTimeout(
|
||||
url,
|
||||
{ method: "GET" },
|
||||
params.timeoutMs,
|
||||
);
|
||||
const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs);
|
||||
if (!res.ok) {
|
||||
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
||||
}
|
||||
|
||||
@@ -10,14 +10,7 @@ export type BlueBubblesReactionOpts = {
|
||||
cfg?: OpenClawConfig;
|
||||
};
|
||||
|
||||
const REACTION_TYPES = new Set([
|
||||
"love",
|
||||
"like",
|
||||
"dislike",
|
||||
"laugh",
|
||||
"emphasize",
|
||||
"question",
|
||||
]);
|
||||
const REACTION_TYPES = new Set(["love", "like", "dislike", "laugh", "emphasize", "question"]);
|
||||
|
||||
const REACTION_ALIASES = new Map<string, string>([
|
||||
// General
|
||||
|
||||
@@ -87,7 +87,9 @@ function extractMessageId(payload: unknown): string {
|
||||
if (!payload || typeof payload !== "object") return "unknown";
|
||||
const record = payload as Record<string, unknown>;
|
||||
const data =
|
||||
record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
|
||||
record.data && typeof record.data === "object"
|
||||
? (record.data as Record<string, unknown>)
|
||||
: null;
|
||||
const candidates = [
|
||||
record.messageId,
|
||||
record.messageGuid,
|
||||
@@ -308,7 +310,11 @@ async function createNewChatWithMessage(params: {
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
// Check for Private API not enabled error
|
||||
if (res.status === 400 || res.status === 403 || errorText.toLowerCase().includes("private api")) {
|
||||
if (
|
||||
res.status === 400 ||
|
||||
res.status === 403 ||
|
||||
errorText.toLowerCase().includes("private api")
|
||||
) {
|
||||
throw new Error(
|
||||
`BlueBubbles send failed: Cannot create new chat - Private API must be enabled. Original error: ${errorText || res.status}`,
|
||||
);
|
||||
|
||||
@@ -20,8 +20,7 @@ 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_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 {
|
||||
|
||||
Reference in New Issue
Block a user