mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 13:00:48 +00:00
178 lines
4.9 KiB
TypeScript
178 lines
4.9 KiB
TypeScript
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
|
|
|
export type BlueBubblesHistoryEntry = {
|
|
sender: string;
|
|
body: string;
|
|
timestamp?: number;
|
|
messageId?: string;
|
|
};
|
|
|
|
export type BlueBubblesHistoryFetchResult = {
|
|
entries: BlueBubblesHistoryEntry[];
|
|
/**
|
|
* True when at least one API path returned a recognized response shape.
|
|
* False means all attempts failed or returned unusable data.
|
|
*/
|
|
resolved: boolean;
|
|
};
|
|
|
|
export type BlueBubblesMessageData = {
|
|
guid?: string;
|
|
text?: string;
|
|
handle_id?: string;
|
|
is_from_me?: boolean;
|
|
date_created?: number;
|
|
date_delivered?: number;
|
|
associated_message_guid?: string;
|
|
sender?: {
|
|
address?: string;
|
|
display_name?: string;
|
|
};
|
|
};
|
|
|
|
export type BlueBubblesChatOpts = {
|
|
serverUrl?: string;
|
|
password?: string;
|
|
accountId?: string;
|
|
timeoutMs?: number;
|
|
cfg?: OpenClawConfig;
|
|
};
|
|
|
|
function resolveAccount(params: BlueBubblesChatOpts) {
|
|
return resolveBlueBubblesServerAccount(params);
|
|
}
|
|
|
|
const MAX_HISTORY_FETCH_LIMIT = 100;
|
|
const HISTORY_SCAN_MULTIPLIER = 8;
|
|
const MAX_HISTORY_SCAN_MESSAGES = 500;
|
|
const MAX_HISTORY_BODY_CHARS = 2_000;
|
|
|
|
function clampHistoryLimit(limit: number): number {
|
|
if (!Number.isFinite(limit)) {
|
|
return 0;
|
|
}
|
|
const normalized = Math.floor(limit);
|
|
if (normalized <= 0) {
|
|
return 0;
|
|
}
|
|
return Math.min(normalized, MAX_HISTORY_FETCH_LIMIT);
|
|
}
|
|
|
|
function truncateHistoryBody(text: string): string {
|
|
if (text.length <= MAX_HISTORY_BODY_CHARS) {
|
|
return text;
|
|
}
|
|
return `${text.slice(0, MAX_HISTORY_BODY_CHARS).trimEnd()}...`;
|
|
}
|
|
|
|
/**
|
|
* Fetch message history from BlueBubbles API for a specific chat.
|
|
* This provides the initial backfill for both group chats and DMs.
|
|
*/
|
|
export async function fetchBlueBubblesHistory(
|
|
chatIdentifier: string,
|
|
limit: number,
|
|
opts: BlueBubblesChatOpts = {},
|
|
): Promise<BlueBubblesHistoryFetchResult> {
|
|
const effectiveLimit = clampHistoryLimit(limit);
|
|
if (!chatIdentifier.trim() || effectiveLimit <= 0) {
|
|
return { entries: [], resolved: true };
|
|
}
|
|
|
|
let baseUrl: string;
|
|
let password: string;
|
|
try {
|
|
({ baseUrl, password } = resolveAccount(opts));
|
|
} catch {
|
|
return { entries: [], resolved: false };
|
|
}
|
|
|
|
// Try different common API patterns for fetching messages
|
|
const possiblePaths = [
|
|
`/api/v1/chat/${encodeURIComponent(chatIdentifier)}/messages?limit=${effectiveLimit}&sort=DESC`,
|
|
`/api/v1/messages?chatGuid=${encodeURIComponent(chatIdentifier)}&limit=${effectiveLimit}`,
|
|
`/api/v1/chat/${encodeURIComponent(chatIdentifier)}/message?limit=${effectiveLimit}`,
|
|
];
|
|
|
|
for (const path of possiblePaths) {
|
|
try {
|
|
const url = buildBlueBubblesApiUrl({ baseUrl, path, password });
|
|
const res = await blueBubblesFetchWithTimeout(
|
|
url,
|
|
{ method: "GET" },
|
|
opts.timeoutMs ?? 10000,
|
|
);
|
|
|
|
if (!res.ok) {
|
|
continue; // Try next path
|
|
}
|
|
|
|
const data = await res.json().catch(() => null);
|
|
if (!data) {
|
|
continue;
|
|
}
|
|
|
|
// Handle different response structures
|
|
let messages: unknown[] = [];
|
|
if (Array.isArray(data)) {
|
|
messages = data;
|
|
} else if (data.data && Array.isArray(data.data)) {
|
|
messages = data.data;
|
|
} else if (data.messages && Array.isArray(data.messages)) {
|
|
messages = data.messages;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
const historyEntries: BlueBubblesHistoryEntry[] = [];
|
|
|
|
const maxScannedMessages = Math.min(
|
|
Math.max(effectiveLimit * HISTORY_SCAN_MULTIPLIER, effectiveLimit),
|
|
MAX_HISTORY_SCAN_MESSAGES,
|
|
);
|
|
for (let i = 0; i < messages.length && i < maxScannedMessages; i++) {
|
|
const item = messages[i];
|
|
const msg = item as BlueBubblesMessageData;
|
|
|
|
// Skip messages without text content
|
|
const text = msg.text?.trim();
|
|
if (!text) {
|
|
continue;
|
|
}
|
|
|
|
const sender = msg.is_from_me
|
|
? "me"
|
|
: msg.sender?.display_name || msg.sender?.address || msg.handle_id || "Unknown";
|
|
const timestamp = msg.date_created || msg.date_delivered;
|
|
|
|
historyEntries.push({
|
|
sender,
|
|
body: truncateHistoryBody(text),
|
|
timestamp,
|
|
messageId: msg.guid,
|
|
});
|
|
}
|
|
|
|
// Sort by timestamp (oldest first for context)
|
|
historyEntries.sort((a, b) => {
|
|
const aTime = a.timestamp || 0;
|
|
const bTime = b.timestamp || 0;
|
|
return aTime - bTime;
|
|
});
|
|
|
|
return {
|
|
entries: historyEntries.slice(0, effectiveLimit), // Ensure we don't exceed the requested limit
|
|
resolved: true,
|
|
};
|
|
} catch (error) {
|
|
// Continue to next path
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If none of the API paths worked, return empty history
|
|
return { entries: [], resolved: false };
|
|
}
|