UNPKG

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
"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