n8n-nodes-zalo-nnt
Version:
Unofficial Zalo integration for n8n - Send messages, manage groups, user operations with QR login. No API key required, works via browser simulation.
250 lines • 9.47 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.pickNextCursors = pickNextCursors;
exports.getChatHistory = getChatHistory;
const zca_js_1 = require("zca-js");
const types_1 = require("./types");
const messageParser_1 = require("./messageParser");
const zaloMessage_1 = require("../../utils/zaloMessage");
const DEFAULT_MAX_PAGES = 50;
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function toMillis(timestamp) {
return typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
}
function filterMessages(messages, params) {
let result = messages;
if (params.filterByThreadId !== false && params.threadId) {
result = result.filter((msg) => msg.threadId === params.threadId);
}
if (params.fromDate || params.toDate) {
result = result.filter((msg) => {
const timestamp = toMillis(msg.timestamp);
if (params.fromDate && timestamp < params.fromDate) {
return false;
}
if (params.toDate && timestamp > params.toDate) {
return false;
}
return true;
});
}
return result;
}
function sortMessages(messages, sortOrder) {
const direction = sortOrder === 'asc' ? 1 : -1;
return messages.sort((a, b) => (0, zaloMessage_1.compareBySortKey)((0, zaloMessage_1.messageSortKey)(a.msgId, a.timestamp), (0, zaloMessage_1.messageSortKey)(b.msgId, b.timestamp), direction));
}
function pickNextCursors(messages) {
const cursors = [];
const seen = new Set();
const add = (candidate) => {
const value = String(candidate !== null && candidate !== void 0 ? candidate : '').trim();
if (!value || seen.has(value))
return;
seen.add(value);
cursors.push(value);
};
const cursorOf = (msg) => String((msg === null || msg === void 0 ? void 0 : msg.msgId) || (msg === null || msg === void 0 ? void 0 : msg.actionId) || (msg === null || msg === void 0 ? void 0 : msg.cliMsgId) || '');
if (messages.length === 0) {
return cursors;
}
let oldest = messages[0];
for (const msg of messages) {
if (toMillis(msg.timestamp) < toMillis(oldest.timestamp)) {
oldest = msg;
}
}
add(cursorOf(oldest));
add(cursorOf(messages[messages.length - 1]));
add(cursorOf(messages[0]));
return cursors;
}
function requestBatch(listener, zcaThreadType, lastMsgId, timeout, allowIdleFinish = false) {
return new Promise((resolve, reject) => {
let settled = false;
let timeoutId;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
listener.removeAllListeners('old_messages');
listener.removeAllListeners('error');
};
const fail = (error) => {
if (settled)
return;
settled = true;
cleanup();
reject(error);
};
const succeed = (messages) => {
if (settled)
return;
settled = true;
cleanup();
resolve(messages);
};
timeoutId = setTimeout(() => {
if (allowIdleFinish) {
succeed([]);
}
else {
fail(new Error(`Request timeout after ${timeout}ms`));
}
}, timeout);
listener.once('error', (error) => fail(error));
listener.once('old_messages', (messages, threadType) => {
if (settled)
return;
try {
const isGroup = threadType === zca_js_1.ThreadType.Group;
succeed((0, messageParser_1.parseMessages)(messages, isGroup));
}
catch (error) {
fail(error);
}
});
try {
listener.requestOldMessages(zcaThreadType, lastMsgId);
}
catch (error) {
fail(error);
}
});
}
async function getChatHistory(api, params, listener, timeout = 30000) {
var _a;
if (params.fetchAll) {
return getChatHistoryAll(api, params, listener, timeout);
}
const zcaThreadType = params.threadType === types_1.ThreadType.User ? zca_js_1.ThreadType.User : zca_js_1.ThreadType.Group;
const rawMessages = await requestBatch(listener, zcaThreadType, params.lastMsgId || null, timeout);
const cursor = (_a = pickNextCursors(rawMessages)[0]) !== null && _a !== void 0 ? _a : null;
const limit = params.limit || 20;
const matched = sortMessages(filterMessages(rawMessages, params), params.sortOrder);
const limitedMessages = matched.slice(0, limit);
if (params.includeReactions && limitedMessages.length > 0) {
await attachReactions(listener, limitedMessages, cursor || '', params.threadType === types_1.ThreadType.Group);
}
return {
messages: limitedMessages,
hasMore: cursor !== null,
lastMsgId: cursor,
totalCount: limitedMessages.length,
fetchedCount: rawMessages.length,
pagesRequested: 1,
threadId: params.threadId,
threadType: params.threadType,
filterByThreadId: params.filterByThreadId !== false,
};
}
async function getChatHistoryAll(api, params, listener, timeout = 30000) {
var _a;
const allMessages = [];
const requestedCursors = new Set();
const delayMs = params.delayBetweenRequests || 1000;
const maxPages = params.maxPages && params.maxPages > 0 ? params.maxPages : DEFAULT_MAX_PAGES;
const zcaThreadType = params.threadType === types_1.ThreadType.User ? zca_js_1.ThreadType.User : zca_js_1.ThreadType.Group;
let currentCursor = params.lastMsgId || null;
let lastCursor = currentCursor;
let pagesRequested = 0;
let exhausted = false;
while (pagesRequested < maxPages) {
if (currentCursor) {
requestedCursors.add(currentCursor);
}
pagesRequested++;
console.log(`Fetching batch ${pagesRequested}/${maxPages}, cursor: ${currentCursor || 'null'}`);
const batch = await requestBatch(listener, zcaThreadType, currentCursor, timeout, pagesRequested > 1);
if (batch.length === 0) {
exhausted = true;
break;
}
allMessages.push(...batch);
const nextCursor = (_a = pickNextCursors(batch).find((c) => !requestedCursors.has(c))) !== null && _a !== void 0 ? _a : null;
if (nextCursor === null) {
exhausted = true;
break;
}
currentCursor = nextCursor;
lastCursor = nextCursor;
await delay(delayMs);
}
if (!exhausted) {
console.warn(`Đã chạm trần ${maxPages} trang — backlog có thể còn. ` +
`Chạy lại với lastMsgId="${lastCursor}" để kéo tiếp.`);
}
const filteredMessages = sortMessages(filterMessages(allMessages, params), params.sortOrder);
if (params.includeReactions && filteredMessages.length > 0) {
await attachReactions(listener, filteredMessages, filteredMessages[filteredMessages.length - 1].msgId, params.threadType === types_1.ThreadType.Group);
}
return {
messages: filteredMessages,
hasMore: !exhausted,
lastMsgId: lastCursor,
totalCount: filteredMessages.length,
fetchedCount: allMessages.length,
pagesRequested,
threadId: params.threadId,
threadType: params.threadType,
filterByThreadId: params.filterByThreadId !== false,
};
}
async function attachReactions(listener, messages, lastMsgId, isGroup) {
try {
const reactions = await getMessageReactions(listener, lastMsgId, isGroup, 5000);
const reactionMap = new Map();
reactions.forEach((reaction) => {
const msgReactions = reactionMap.get(reaction.msgId) || [];
msgReactions.push(reaction);
reactionMap.set(reaction.msgId, msgReactions);
});
messages.forEach((msg) => {
msg.reactions = reactionMap.get(msg.msgId);
});
}
catch (error) {
console.warn('Failed to fetch reactions:', error);
}
}
async function getMessageReactions(listener, lastMsgId, isGroup, timeout = 5000) {
return new Promise((resolve) => {
let resolved = false;
let timeoutId;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
listener.removeAllListeners('old_reactions');
};
timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
cleanup();
resolve([]);
}
}, timeout);
listener.once('old_reactions', (reactions) => {
if (resolved)
return;
resolved = true;
cleanup();
const parsed = reactions.map(messageParser_1.parseReaction);
resolve(parsed);
});
try {
const threadType = isGroup ? zca_js_1.ThreadType.Group : zca_js_1.ThreadType.User;
listener.requestOldReactions(threadType, lastMsgId);
}
catch (error) {
if (!resolved) {
resolved = true;
cleanup();
resolve([]);
}
}
});
}
//# sourceMappingURL=historyProcessor.js.map