n8n-nodes-whatsapp-media-processor
Version:
Node n8n para processamento de mídia do WhatsApp com OpenAI
702 lines • 33 kB
JavaScript
;
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