UNPKG

n8n-nodes-whatsapp-media-processor

Version:
702 lines 33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WhatsappMediaProcessor = void 0; const n8n_workflow_1 = require("n8n-workflow"); const axios_1 = __importDefault(require("axios")); const openai_1 = require("openai"); const pdf_parse_1 = __importDefault(require("pdf-parse")); class WhatsappMediaProcessor { constructor() { this.description = { displayName: 'WhatsApp Media Processor', name: 'whatsappMediaProcessor', icon: 'file:whatsapp.svg', group: ['transform'], version: 1, description: 'Processa mídia do WhatsApp usando OpenAI', defaults: { name: 'WhatsApp Media Processor', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'whatsappMediaProcessorApi', required: true, }, ], properties: [ { displayName: 'Operação', name: 'operation', type: 'options', options: [ { name: 'Processar Mídia', value: 'processMedia', }, { name: 'Dividir Mensagem', value: 'splitMessage', }, { name: 'Agrupar Mensagens Automático', value: 'autoGroupMessages', }, ], default: 'processMedia', description: 'Operação a ser executada', }, { displayName: 'Processar Texto', name: 'processText', type: 'boolean', default: true, description: 'Ativar processamento de mensagens de texto', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Processar Áudio', name: 'processAudio', type: 'boolean', default: true, description: 'Ativar processamento de mensagens de áudio', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Modelo para Áudio', name: 'audioModel', type: 'options', options: [ { name: 'Whisper-1', value: 'whisper-1', }, ], default: 'whisper-1', description: 'Modelo da OpenAI para transcrição de áudio', displayOptions: { show: { operation: [ 'processMedia', ], processAudio: [ true, ], }, }, }, { displayName: 'Processar Imagem', name: 'processImage', type: 'boolean', default: true, description: 'Ativar processamento de mensagens de imagem', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Prompt para Análise de Imagem', name: 'imagePrompt', type: 'string', default: 'Descreva detalhadamente esta imagem e extraia qualquer texto visível.', description: 'Prompt personalizado para análise da imagem', displayOptions: { show: { operation: [ 'processMedia', ], processImage: [ true, ], }, }, }, { displayName: 'Modelo para Imagem', name: 'imageModel', type: 'options', options: [ { name: 'GPT-4 Vision', value: 'gpt-4-vision-preview', }, { name: 'GPT-4o', value: 'gpt-4o', }, { name: 'GPT-4o-mini', value: 'gpt-4o-mini', }, { name: 'ChatGPT-4o-latest', value: 'gpt-4o-2024-05-13', }, ], default: 'gpt-4o', description: 'Modelo da OpenAI para análise de imagem', displayOptions: { show: { operation: [ 'processMedia', ], processImage: [ true, ], }, }, }, { displayName: 'Qualidade da Imagem', name: 'imageQuality', type: 'options', options: [ { name: 'Auto', value: 'auto', }, { name: 'Baixa', value: 'low', }, { name: 'Alta', value: 'high', }, ], default: 'auto', description: 'Qualidade da imagem para processamento', displayOptions: { show: { operation: [ 'processMedia', ], processImage: [ true, ], }, }, }, { displayName: 'Mensagem', name: 'message', type: 'string', default: '', required: true, description: 'Mensagem recebida do cliente', displayOptions: { show: { operation: [ 'waitForMessages', ], }, }, }, { displayName: 'Telefone', name: 'phone', type: 'string', default: '', required: true, description: 'Número de telefone do cliente', displayOptions: { show: { operation: [ 'waitForMessages', ], }, }, }, { displayName: 'Tempo de Espera (segundos)', name: 'waitTime', type: 'number', typeOptions: { minValue: 1, }, default: 5, description: 'Tempo de espera para agrupar mensagens picotadas do mesmo usuário', displayOptions: { show: { operation: [ 'waitForMessages', ], }, }, }, { displayName: 'Processar PDF', name: 'processPdf', type: 'boolean', default: true, description: 'Ativar processamento de documentos PDF', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Timeout (ms)', name: 'timeout', type: 'number', default: 30000, description: 'Tempo máximo de espera para downloads', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Tentativas de Retry', name: 'maxRetries', type: 'number', default: 3, description: 'Número máximo de tentativas em caso de falha', displayOptions: { show: { operation: [ 'processMedia', ], }, }, }, { displayName: 'Mensagem para Dividir', name: 'splitMessageText', type: 'string', default: '', required: true, description: 'Texto que será dividido em partes menores', displayOptions: { show: { operation: [ 'splitMessage', ], }, }, }, { displayName: 'Telefone', name: 'splitPhone', type: 'string', default: '', required: true, description: 'Número de telefone a ser associado com cada parte da mensagem', displayOptions: { show: { operation: [ 'splitMessage', ], }, }, }, { displayName: 'Limite de Palavras', name: 'wordLimit', type: 'number', typeOptions: { minValue: 1, }, default: 30, description: 'Número máximo de palavras por parte da mensagem', displayOptions: { show: { operation: [ 'splitMessage', ], }, }, }, { displayName: 'Número de Telefone', name: 'autoGroupPhone', type: 'string', required: true, default: '', description: 'Número de telefone do cliente', displayOptions: { show: { operation: [ 'autoGroupMessages', ], }, }, }, { displayName: 'Mensagem', name: 'autoGroupMessage', type: 'string', default: '', required: true, description: 'Mensagem recebida do cliente', displayOptions: { show: { operation: [ 'autoGroupMessages', ], }, }, }, { displayName: 'Tempo de Espera (segundos)', name: 'autoGroupTime', type: 'number', typeOptions: { minValue: 1, }, default: 5, description: 'Tempo para aguardar e agrupar mensagens do mesmo número', displayOptions: { show: { operation: [ 'autoGroupMessages', ], }, }, }, ], }; } static getInstance() { if (!WhatsappMediaProcessor.instance) { WhatsappMediaProcessor.instance = new WhatsappMediaProcessor(); } return WhatsappMediaProcessor.instance; } async execute() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r; const credentials = await this.getCredentials('whatsappMediaProcessorApi'); const items = this.getInputData(); const returnData = []; // Verificar a operação selecionada const operation = this.getNodeParameter('operation', 0); // Mapa para armazenar mensagens agrupadas por telefone const messageMap = {}; if (operation === 'autoGroupMessages') { // Implementação da função "Agrupar Mensagens Automático" // Esta função usa variáveis de workflow do n8n para armazenar mensagens entre execuções // e automaticamente gerencia o ciclo de vida das mensagens // Obter as variáveis de workflow const workflowData = this.getWorkflowStaticData('global'); // Inicializar as estruturas de dados se não existirem if (!workflowData.messageGroups) { workflowData.messageGroups = {}; } if (!workflowData.lastUpdated) { workflowData.lastUpdated = {}; } // Processar cada item de entrada for (let i = 0; i < items.length; i++) { try { const phone = this.getNodeParameter('autoGroupPhone', i); const message = this.getNodeParameter('autoGroupMessage', i); const waitTime = this.getNodeParameter('autoGroupTime', i); const waitTimeMs = waitTime * 1000; const now = Date.now(); // Inicializar o grupo para este número se não existir if (!workflowData.messageGroups[phone]) { workflowData.messageGroups[phone] = []; workflowData.lastUpdated[phone] = now; } // Adicionar a mensagem ao grupo workflowData.messageGroups[phone].push(message.trim()); // Verificar se já existe um timestamp para este número const existingTimestamp = workflowData.lastUpdated[phone] || 0; const messageCount = workflowData.messageGroups[phone].length; // Se for a primeira mensagem, atualizar o timestamp if (messageCount === 1) { workflowData.lastUpdated[phone] = now; } // Verificar todos os números para processamento const phonesToProcess = []; for (const p in workflowData.messageGroups) { if (workflowData.messageGroups[p].length > 0) { const lastUpdate = workflowData.lastUpdated[p] || 0; const elapsed = now - lastUpdate; // Processar se o tempo expirou OU se for uma mensagem única e o tempo for zero if (elapsed >= waitTimeMs || (waitTime === 0 && workflowData.messageGroups[p].length === 1)) { phonesToProcess.push(p); } } } // Processar os números que já passaram do tempo de espera for (const p of phonesToProcess) { const messages = workflowData.messageGroups[p]; const combinedMessage = messages.join(' '); // Retornar phone e message no output returnData.push({ json: { phone: p, message: combinedMessage, }, }); // Limpar o grupo para este número workflowData.messageGroups[p] = []; delete workflowData.lastUpdated[p]; } // Não retornar nada se não houver mensagens para processar // Isso garante que o fluxo só tenha saída quando as mensagens forem agrupadas } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Erro ao agrupar mensagens: ${(error === null || error === void 0 ? void 0 : error.message) || 'Erro desconhecido'}`); } } return [returnData]; } else if (operation === 'splitMessage') { // Implementação da função "Dividir Mensagem" for (let i = 0; i < items.length; i++) { try { const phone = this.getNodeParameter('splitPhone', i); const fullMessage = this.getNodeParameter('splitMessageText', i); const wordLimit = this.getNodeParameter('wordLimit', i); // Função para dividir a mensagem em partes com base em sinais de pontuação const splitByPunctuation = (text) => { // Divide a mensagem por ponto, exclamação ou interrogação seguido de espaço const sentences = text.split(/(?<=[.!?])\s+/); const result = []; // Adiciona cada sentença ao resultado for (const sentence of sentences) { result.push(sentence.trim()); } return result; }; // Contar o número de palavras na mensagem const wordsCount = fullMessage.split(/\s+/).length; // Verificar se a mensagem precisa ser dividida let parts; if (wordsCount <= wordLimit) { // Se a mensagem for menor ou igual ao limite, mantê-la inteira parts = [fullMessage]; } else { // Dividir a mensagem em partes com base em pontuação parts = splitByPunctuation(fullMessage); } // Adicionar cada parte como um item separado no resultado for (const part of parts) { if (part.trim()) { // Ignorar partes vazias returnData.push({ json: { phone: phone, message: part.trim(), }, }); } } } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Erro ao dividir mensagem: ${(error === null || error === void 0 ? void 0 : error.message) || 'Erro desconhecido'}`); } } return [returnData]; } // Processar Mídia (operação original) const openai = new openai_1.OpenAI({ apiKey: credentials.openaiApiKey, }); for (let i = 0; i < items.length; i++) { try { const item = items[i]; const webhook = item.json; const webhookData = (_e = (_d = (_c = (_b = (_a = webhook.body) === null || _a === void 0 ? void 0 : _a.entry) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.changes) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.value; if (!((_f = webhookData === null || webhookData === void 0 ? void 0 : webhookData.messages) === null || _f === void 0 ? void 0 : _f[0])) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Formato de webhook inválido'); } const message = webhookData.messages[0]; // Extrair o wa_id do contato const phone = ((_h = (_g = webhookData.contacts) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.wa_id) || message.from || ''; let resultText = ''; switch (message.type) { case 'text': if (this.getNodeParameter('processText', i)) { resultText = ((_j = message.text) === null || _j === void 0 ? void 0 : _j.body) || ''; } else { resultText = 'Processamento de texto desativado'; } break; case 'audio': if (this.getNodeParameter('processAudio', i)) { if (!((_k = message.audio) === null || _k === void 0 ? void 0 : _k.id)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Áudio inválido'); } const audioUrl = await WhatsappMediaProcessor.getMediaUrl(message.audio.id, credentials); const audioResponse = await WhatsappMediaProcessor.downloadMedia(audioUrl, credentials); const file = new File([audioResponse], 'audio.mp3', { type: 'audio/mp3' }); const transcription = await openai.audio.transcriptions.create({ file, model: this.getNodeParameter('audioModel', i), }); resultText = transcription.text; } else { resultText = 'Processamento de áudio desativado'; } break; case 'image': if (this.getNodeParameter('processImage', i)) { if (!((_l = message.image) === null || _l === void 0 ? void 0 : _l.id)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Imagem inválida'); } const imageUrl = await WhatsappMediaProcessor.getMediaUrl(message.image.id, credentials); const imageResponse = await WhatsappMediaProcessor.downloadMedia(imageUrl, credentials); // Obter a qualidade da imagem selecionada const imageQuality = this.getNodeParameter('imageQuality', i); // Converter a imagem para base64 const base64Image = imageResponse.toString('base64'); // Determinar qual modelo está sendo usado const selectedModel = this.getNodeParameter('imageModel', i); const prompt = this.getNodeParameter('imagePrompt', i); // Criar a requisição para a API da OpenAI let response; // Usar a API HTTP diretamente para evitar problemas de tipagem const requestData = { model: selectedModel, messages: [ { role: 'user', content: [ { type: 'text', text: prompt, }, { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}`, detail: imageQuality, }, }, ], }, ], }; const apiResponse = await axios_1.default.post('https://api.openai.com/v1/chat/completions', requestData, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${credentials.openaiApiKey}`, }, }); if ((_q = (_p = (_o = (_m = apiResponse.data) === null || _m === void 0 ? void 0 : _m.choices) === null || _o === void 0 ? void 0 : _o[0]) === null || _p === void 0 ? void 0 : _p.message) === null || _q === void 0 ? void 0 : _q.content) { resultText = apiResponse.data.choices[0].message.content; } else { throw new Error('Resposta inválida da API da OpenAI'); } } else { resultText = 'Processamento de imagem desativado'; } break; case 'document': if (this.getNodeParameter('processPdf', i)) { if (!((_r = message.document) === null || _r === void 0 ? void 0 : _r.id)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Documento inválido'); } if (message.document.mime_type === 'application/pdf') { const pdfUrl = await WhatsappMediaProcessor.getMediaUrl(message.document.id, credentials); const pdfResponse = await WhatsappMediaProcessor.downloadMedia(pdfUrl, credentials); const pdfData = await (0, pdf_parse_1.default)(pdfResponse); resultText = pdfData.text; } else { resultText = `Documento recebido: ${message.document.filename || 'Sem nome'} (${message.document.mime_type})`; } } else { resultText = 'Processamento de PDF desativado'; } break; default: throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Tipo de mídia '${message.type}' não suportado`); } returnData.push({ json: { phone: phone, message: resultText, }, }); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Erro no processamento: ${(error === null || error === void 0 ? void 0 : error.message) || 'Erro desconhecido'}`); } } return [returnData]; } static async getMediaUrl(mediaId, credentials) { var _a, _b, _c, _d; try { if (!credentials.whatsappBusinessId) { throw new Error('WhatsApp Business ID não configurado'); } if (!credentials.whatsappApiKey) { throw new Error('WhatsApp API Key não configurada'); } const url = `https://graph.facebook.com/v22.0/${mediaId}`; console.log('Requesting media URL:', url); const response = await axios_1.default.get(url, { headers: { Authorization: `Bearer ${credentials.whatsappApiKey}`, }, }); if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.url)) { throw new Error('URL da mídia não encontrada na resposta'); } return response.data.url; } catch (error) { throw new Error(`Erro ao obter URL da mídia: ${((_d = (_c = (_b = error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.error) === null || _d === void 0 ? void 0 : _d.message) || error.message}`); } } static async downloadMedia(url, credentials) { var _a, _b, _c; try { console.log('Downloading media from:', url); // Primeiro, verificamos se a URL ainda é válida (expira em 5 minutos) const response = await axios_1.default.get(url, { responseType: 'arraybuffer', headers: { Authorization: `Bearer ${credentials.whatsappApiKey}`, }, }); // Se a resposta for 404, tentamos obter uma nova URL if (response.status === 404) { throw new Error('URL da mídia expirada. Por favor, tente novamente.'); } return Buffer.from(response.data); } catch (error) { throw new Error(`Erro ao baixar mídia: ${((_c = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.message) || error.message}`); } } } exports.WhatsappMediaProcessor = WhatsappMediaProcessor; //# sourceMappingURL=WhatsappMediaProcessor.node.js.map