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.
469 lines • 27.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZaloSendMessage = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const zca_js_1 = require("zca-js");
const helper_1 = require("../utils/helper");
const ZaloSendMessage_properties_1 = require("./ZaloSendMessage.properties");
const attachmentProcessor_1 = require("./utils/attachmentProcessor");
const messageBuilder_1 = require("./utils/messageBuilder");
const validator_1 = require("./utils/validator");
const markdownToZaloStyles_1 = require("./utils/markdownToZaloStyles");
const mentionParser_1 = require("./utils/mentionParser");
const videoProcessor_1 = require("./utils/videoProcessor");
let api;
class ZaloSendMessage {
constructor() {
this.description = {
displayName: 'Zalo Gửi tin nhắn by diginno.net',
name: 'zaloSendMessage',
icon: 'file:../shared/image.svg',
group: ['Zalo'],
version: 5,
description: 'Gửi tin nhắn qua API Zalo sử dụng kết nối đăng nhập bằng cookie by diginno.net',
defaults: {
name: 'Zalo Gửi tin nhắn by diginno.net',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'zaloApi',
required: true,
},
],
properties: ZaloSendMessage_properties_1.zaloSendMessageProperties,
};
}
async execute() {
const returnData = [];
const items = this.getInputData();
const zaloCred = await this.getCredentials('zaloApi');
let cookieFromCred = zaloCred.cookie;
try {
cookieFromCred = JSON.parse(zaloCred.cookie);
}
catch {
}
const imeiFromCred = zaloCred.imei;
const userAgentFromCred = zaloCred.userAgent;
try {
const zalo = new zca_js_1.Zalo({
imageMetadataGetter: helper_1.imageMetadataGetter,
});
api = await zalo.login({
cookie: cookieFromCred,
imei: imeiFromCred,
userAgent: userAgentFromCred,
});
if (!api) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check your credentials.');
}
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
}
for (let i = 0; i < items.length; i++) {
const downloadedFiles = [];
try {
const threadId = this.getNodeParameter('threadId', i);
const typeNumber = this.getNodeParameter('type', i);
const type = typeNumber === 0 ? zca_js_1.ThreadType.User : zca_js_1.ThreadType.Group;
const messageType = this.getNodeParameter('messageType', i, 'text');
(0, validator_1.validateThreadId)(threadId);
if (!api) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Zalo API not initialized');
}
try {
const result = await api.sendTypingEvent(threadId, type);
if (!!result) {
this.logger.info('Send! typing event');
}
}
catch (e) {
this.logger.error('Cannot send typing event');
}
if (messageType === 'video') {
const videoSource = this.getNodeParameter('videoSource', i, 'url');
const videoMessage = this.getNodeParameter('videoMessage', i, '');
const ttl = this.getNodeParameter('ttl', i, 0);
const videoFilesToCleanup = [];
if (videoSource === 'binary') {
const binaryPropertyName = this.getNodeParameter('videoBinaryProperty', i, 'data');
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
const autoVideoMetadata = this.getNodeParameter('autoVideoMetadata', i, true);
let videoDuration = this.getNodeParameter('videoDuration', i, 0);
let videoWidth = this.getNodeParameter('videoWidth', i, 1280);
let videoHeight = this.getNodeParameter('videoHeight', i, 720);
const fileName = binaryData.fileName || 'video.mp4';
this.logger.info(`[Video Binary] Processing binary video: ${fileName}`);
const videoBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
this.logger.info(`[Video Binary] Video size: ${videoBuffer.length} bytes`);
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const os = await Promise.resolve().then(() => __importStar(require('os')));
const tempDir = process.env.N8N_USER_FOLDER
? path.default.join(process.env.N8N_USER_FOLDER, 'temp_videos')
: path.default.join(os.default.homedir(), '.n8n', 'temp_videos');
if (!fs.default.existsSync(tempDir)) {
fs.default.mkdirSync(tempDir, { recursive: true });
}
const tempVideoPath = path.default.join(tempDir, `binary-${Date.now()}.mp4`);
fs.default.writeFileSync(tempVideoPath, videoBuffer);
videoFilesToCleanup.push(tempVideoPath);
this.logger.info(`[Video Binary] Saved to temp: ${tempVideoPath}`);
try {
this.logger.info('[Video Binary] Uploading video to catbox.moe...');
const FormData = (await Promise.resolve().then(() => __importStar(require('form-data')))).default;
const axios = await Promise.resolve().then(() => __importStar(require('axios')));
const formData = new FormData();
formData.append('reqtype', 'fileupload');
formData.append('fileToUpload', fs.default.createReadStream(tempVideoPath));
const uploadResponse = await axios.default.post('https://catbox.moe/user/api.php', formData, {
headers: formData.getHeaders(),
timeout: 300000,
maxContentLength: 200 * 1024 * 1024,
maxBodyLength: 200 * 1024 * 1024,
});
const videoUrl = uploadResponse.data;
if (!videoUrl || !videoUrl.startsWith('https://')) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Upload to catbox failed: ${uploadResponse.data}`);
}
this.logger.info(`[Video Binary] Video uploaded: ${videoUrl}`);
const { getPublicThumbnailUrl: getThumbnail } = await Promise.resolve().then(() => __importStar(require('./utils/videoProcessor')));
const thumbnailResult = await getThumbnail(tempVideoPath, '', this.logger);
if (thumbnailResult.localPath) {
videoFilesToCleanup.push(thumbnailResult.localPath);
}
if (autoVideoMetadata && thumbnailResult.metadata) {
const meta = thumbnailResult.metadata;
if (meta.duration > 0 && videoDuration === 0) {
videoDuration = meta.duration;
}
if (meta.width > 0 && meta.height > 0) {
videoWidth = meta.width;
videoHeight = meta.height;
}
this.logger.info(`[Video Binary] Metadata: ${videoWidth}x${videoHeight}, duration=${videoDuration}ms`);
}
const finalThumbnailUrl = thumbnailResult.url;
if (!finalThumbnailUrl) {
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
throw new n8n_workflow_1.NodeOperationError(this.getNode(), thumbnailResult.diagnosis ||
'Could not generate a thumbnail for this video.');
}
const videoOptions = {
videoUrl,
thumbnailUrl: finalThumbnailUrl,
msg: videoMessage || '',
duration: videoDuration > 0 ? videoDuration : 0,
width: videoWidth || 1280,
height: videoHeight || 720,
ttl: ttl > 0 ? ttl : 0,
};
this.logger.info(`[Video Binary] Sending video: ${JSON.stringify(videoOptions)}`);
const response = await api.sendVideo(videoOptions, threadId, type);
this.logger.info(`[Video Binary] sendVideo response: ${JSON.stringify(response)}`);
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
returnData.push({
json: {
success: true,
threadId,
threadType: type,
messageType: 'video',
source: 'binary',
fileName,
fileSize: videoBuffer.length,
uploadedUrl: videoUrl,
videoOptions,
response: response || {},
},
});
}
catch (uploadError) {
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
const errorMessage = uploadError.message || 'Unknown error';
this.logger.error(`[Video Binary] Failed: ${errorMessage}`);
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Video upload failed: ${errorMessage}`, {
itemIndex: i,
});
}
}
else {
const videoUrl = this.getNodeParameter('videoUrl', i);
const userThumbnailUrl = this.getNodeParameter('thumbnailUrl', i, '');
const autoVideoMetadata = this.getNodeParameter('autoVideoMetadata', i, true);
let videoDuration = this.getNodeParameter('videoDuration', i, 0);
let videoWidth = this.getNodeParameter('videoWidth', i, 1280);
let videoHeight = this.getNodeParameter('videoHeight', i, 720);
if (!videoUrl || videoUrl.trim() === '') {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Video URL is required');
}
this.logger.info('[Video URL] Pre-validating video URL...');
try {
const axios = await Promise.resolve().then(() => __importStar(require('axios')));
const headResponse = await axios.default.head(videoUrl.trim(), {
timeout: 15000,
maxRedirects: 5,
validateStatus: (status) => status < 400,
});
const videoFileSize = parseInt(headResponse.headers['content-length'] || '0', 10);
const contentType = headResponse.headers['content-type'] || '';
this.logger.info(`[Video URL] Video URL valid: size=${videoFileSize} bytes, type=${contentType}`);
}
catch (urlError) {
this.logger.warn(`[Video URL] Video URL may not be accessible: ${urlError.message}`);
}
this.logger.info('[Video URL] Processing thumbnail...');
const thumbnailResult = await (0, videoProcessor_1.getPublicThumbnailUrl)(videoUrl.trim(), userThumbnailUrl, this.logger);
if (thumbnailResult.localPath) {
videoFilesToCleanup.push(thumbnailResult.localPath);
}
if (thumbnailResult.videoPath) {
videoFilesToCleanup.push(thumbnailResult.videoPath);
}
if (autoVideoMetadata && thumbnailResult.metadata) {
const meta = thumbnailResult.metadata;
if (meta.duration > 0 && videoDuration === 0) {
videoDuration = meta.duration;
this.logger.info(`[Video URL] Auto-detected duration: ${videoDuration}ms`);
}
if (meta.width > 0 && meta.height > 0) {
videoWidth = meta.width;
videoHeight = meta.height;
this.logger.info(`[Video URL] Auto-detected dimensions: ${videoWidth}x${videoHeight}`);
}
}
const finalThumbnailUrl = thumbnailResult.url;
if (!finalThumbnailUrl) {
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
throw new n8n_workflow_1.NodeOperationError(this.getNode(), thumbnailResult.diagnosis ||
'Could not get a thumbnail URL for this video. Please provide a thumbnail URL manually.');
}
const videoOptions = {
videoUrl: videoUrl.trim(),
thumbnailUrl: finalThumbnailUrl,
msg: videoMessage || '',
duration: videoDuration > 0 ? videoDuration : 0,
width: videoWidth || 1280,
height: videoHeight || 720,
ttl: ttl > 0 ? ttl : 0,
};
this.logger.info(`[Video URL] Sending video with options: ${JSON.stringify(videoOptions)}`);
try {
this.logger.info('[Video URL] Calling api.sendVideo...');
const response = await api.sendVideo(videoOptions, threadId, type);
this.logger.info(`[Video URL] sendVideo response: ${JSON.stringify(response)}`);
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
returnData.push({
json: {
success: true,
threadId,
threadType: type,
messageType: 'video',
source: 'url',
videoOptions,
autoDetectedMetadata: thumbnailResult.metadata,
response: response || {},
},
});
}
catch (videoError) {
const errorMessage = videoError.message || 'Unknown error';
this.logger.error(`[Video URL] sendVideo failed: ${errorMessage}`);
(0, videoProcessor_1.cleanupVideoFiles)(videoFilesToCleanup, this.logger);
let helpfulMessage = errorMessage;
if (errorMessage.includes('Tham số không hợp lệ') ||
errorMessage.includes('Invalid parameter')) {
helpfulMessage =
`${errorMessage}. Possible causes:\n` +
'1. Video URL is not publicly accessible (Zalo servers need to download it)\n' +
'2. Thumbnail URL is not accessible or invalid\n' +
'3. Video format may not be supported\n' +
`Video URL: ${videoOptions.videoUrl}\n` +
`Thumbnail URL: ${videoOptions.thumbnailUrl}`;
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), helpfulMessage, { itemIndex: i });
}
}
}
else if (messageType === 'card') {
const cardUserId = this.getNodeParameter('cardUserId', i);
const cardPhoneNumber = this.getNodeParameter('cardPhoneNumber', i, '');
const cardTtl = this.getNodeParameter('cardTtl', i, 0);
const response = await api.sendCard({
userId: cardUserId,
...(cardPhoneNumber && { phoneNumber: cardPhoneNumber }),
...(cardTtl > 0 && { ttl: cardTtl }),
}, threadId, type);
returnData.push({
json: { success: true, threadId, threadType: type, messageType: 'card', response },
});
}
else if (messageType === 'bankCard') {
const bankCardBin = this.getNodeParameter('bankCardBin', i);
const bankCardAccNum = this.getNodeParameter('bankCardAccNum', i);
const bankCardAccName = this.getNodeParameter('bankCardAccName', i, '');
const response = await api.sendBankCard({
binBank: bankCardBin,
numAccBank: bankCardAccNum,
...(bankCardAccName && { nameAccBank: bankCardAccName }),
}, threadId, type);
returnData.push({
json: { success: true, threadId, threadType: type, messageType: 'bankCard', response },
});
}
else if (messageType === 'link') {
const linkUrl = this.getNodeParameter('linkUrl', i);
const linkMsg = this.getNodeParameter('linkMsg', i, '');
const linkTtl = this.getNodeParameter('linkTtl', i, 0);
const response = await api.sendLink({
link: linkUrl,
...(linkMsg && { msg: linkMsg }),
...(linkTtl > 0 && { ttl: linkTtl }),
}, threadId, type);
returnData.push({
json: { success: true, threadId, threadType: type, messageType: 'link', response },
});
}
else if (messageType === 'voice') {
const voiceUrl = this.getNodeParameter('voiceUrl', i);
const voiceTtl = this.getNodeParameter('voiceTtl', i, 0);
const response = await api.sendVoice({
voiceUrl,
...(voiceTtl > 0 && { ttl: voiceTtl }),
}, threadId, type);
returnData.push({
json: { success: true, threadId, threadType: type, messageType: 'voice', response },
});
}
else if (messageType === 'upload') {
const uploadUrl = this.getNodeParameter('uploadUrl', i);
const response = await api.uploadAttachment(uploadUrl, threadId, type);
returnData.push({
json: { success: true, threadId, threadType: type, messageType: 'upload', response },
});
}
else {
let message = this.getNodeParameter('message', i);
const enableMarkdown = this.getNodeParameter('enableMarkdown', i, false);
const urgency = this.getNodeParameter('urgency', i, 0);
const ttl = this.getNodeParameter('ttl', i, 0);
const quoteFields = this.getNodeParameter('quoteFields', i, {});
const attachments = this.getNodeParameter('attachments', i, {});
let markdownStyles = [];
if (enableMarkdown && message) {
const result = (0, markdownToZaloStyles_1.markdownToZaloStyles)(message);
message = result.plainText;
markdownStyles = result.styles;
this.logger.info(`Markdown enabled: converted ${markdownStyles.length} styles`);
}
let parsedMentions = [];
if (message && type === zca_js_1.ThreadType.Group) {
const mentionParseResult = (0, mentionParser_1.parseMentionsFromText)(message);
message = mentionParseResult.plainText;
parsedMentions = mentionParseResult.mentions;
if (parsedMentions.length > 0) {
this.logger.info(`Parsed ${parsedMentions.length} mentions from message text`);
}
}
const attachmentResult = await (0, attachmentProcessor_1.processAttachments)(attachments, this.logger);
downloadedFiles.push(...attachmentResult.downloadedFiles);
const allFiles = [...attachmentResult.downloadedFiles, ...attachmentResult.localFiles];
this.logger.info(`Attachment processing: downloaded=${attachmentResult.downloadedFiles.length}, local=${attachmentResult.localFiles.length}, errors=${attachmentResult.errors.length}`);
if (allFiles.length > 0) {
this.logger.info(`Total files to send: ${allFiles.length}`);
console.log('[ZaloSendMessage] Files to send:', allFiles);
}
if (attachmentResult.errors.length > 0) {
this.logger.warn(`Some attachments failed: ${attachmentResult.errors.join(', ')}`);
}
const messageParams = {
message,
urgency: urgency !== 0 ? urgency : undefined,
ttl: ttl > 0 ? ttl : undefined,
styles: markdownStyles.length > 0 ? markdownStyles : undefined,
attachments: allFiles.length > 0 ? allFiles : undefined,
};
if (quoteFields && quoteFields.quote) {
messageParams.quote = quoteFields.quote;
}
if (parsedMentions.length > 0) {
messageParams.mentions = parsedMentions;
this.logger.info(`Mentions to send: ${parsedMentions.length}`);
}
const messageContent = (0, messageBuilder_1.buildMessageContent)(messageParams, this.logger);
this.logger.info(`Sending message with parameters: ${JSON.stringify(messageContent)}`);
console.log('[ZaloSendMessage] Full messageContent:', JSON.stringify(messageContent, null, 2));
console.log('[ZaloSendMessage] threadId:', threadId, 'type:', type);
const response = await api.sendMessage(messageContent, threadId, type);
this.logger.info(`Zalo API response: ${JSON.stringify(response)}`);
await (0, attachmentProcessor_1.cleanupFiles)(downloadedFiles, this.logger);
this.logger.info('Message sent successfully', { threadId, type });
const responseData = {
success: true,
threadId,
threadType: type,
messageType: 'text',
messageContent,
response: response || {},
};
returnData.push({
json: responseData,
});
}
}
catch (error) {
this.logger.error('Error sending Zalo message:', error);
await (0, attachmentProcessor_1.cleanupFiles)(downloadedFiles, this.logger);
if (this.continueOnFail()) {
returnData.push({
json: {
success: false,
error: error.message,
},
});
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
}
}
}
return [returnData];
}
}
exports.ZaloSendMessage = ZaloSendMessage;
//# sourceMappingURL=ZaloSendMessage.node.js.map