n8n-nodes-japi
Version:
Node comunitário para integrar a J-API (WhatsApp) ao n8n
611 lines (587 loc) • 17 kB
text/typescript
import {
IExecuteFunctions,
NodeApiError,
} from 'n8n-workflow';
export class JApi {
description = {
displayName: 'J-API',
name: 'japi',
group: ['output'],
version: 1,
icon: 'file:japi.svg',
description: 'Interage com a API J-API (WhatsApp)',
defaults: { name: 'J-API' },
inputs: ['main'],
outputs: ['main'],
credentials: [{ name: 'japiApi', required: true }],
/*────────── UI fields ──────────*/
properties: [
/* 1. seletor da ação */
{
displayName: 'Tipo de envio',
name: 'action',
type: 'options',
options: [
{ name: 'Texto', value: 'text' },
{ name: 'Mídia', value: 'media' },
{ name: 'Contato', value: 'contact' },
{ name: 'Localização', value: 'location' },
{ name: 'Status', value: 'status' },
{ name: 'Menu', value: 'menu' },
{ name: 'Carousel', value: 'carousel' },
],
default: 'text',
},
/* 2. campos compartilhados */
{
displayName: 'Telefone (E.164)',
name: 'phone',
type: 'string',
displayOptions: {
show: {
action: [
'text',
'media',
'contact',
'location',
'menu',
'carousel',
],
},
},
default: '',
required: true,
},
/* 3. campos específicos */
/* Texto */
{
displayName: 'Mensagem de texto',
name: 'text',
type: 'string',
displayOptions: { show: { action: ['text', 'status'] } },
default: '',
required: true,
},
/* Mídia */
/*{
displayName: 'URL da mídia',
name: 'mediaUrl',
type: 'string',
displayOptions: { show: { action: ['media'] } },
default: '',
required: true,
},*/
{
displayName: 'Legenda (opcional)',
name: 'caption',
type: 'string',
displayOptions: { show: { action: ['media'] } },
default: '',
},
{
displayName: 'Tipo de mídia',
name: 'mediaType',
type: 'options',
displayOptions: { show: { action: ['media'] } },
options: [
{ name: 'Imagem', value: 'image' },
{ name: 'Vídeo', value: 'video' },
{ name: 'Documento',value: 'document'},
{ name: 'Áudio', value: 'audio' },
{ name: 'MyAudio', value: 'myaudio' },
{ name: 'PTT', value: 'ptt' },
{ name: 'Sticker', value: 'sticker' },
],
default: 'image',
required: true,
},
{
displayName: 'Arquivo (URL ou base64)',
name: 'file',
type: 'string',
displayOptions: { show: { action: ['media'] } },
default: '',
required: true,
},
{
displayName: 'Nome do doc (apenas documents)',
name: 'docName',
type: 'string',
displayOptions: { show: { action: ['media'] } },
default: '',
},
{
displayName: 'MIME type (opcional)',
name: 'mimetype',
type: 'string',
displayOptions: { show: { action: ['media'] } },
default: '',
},
/* Campos compartilhados */
{
displayName: 'Reply ID',
name: 'replyid',
type: 'string',
displayOptions: {
show: {
action: ['media', 'contact']
}
},
default: '',
description: 'ID da mensagem para responder',
},
{
displayName: 'Menções',
name: 'mentions',
type: 'string',
displayOptions: {
show: {
action: ['media', 'contact']
}
},
default: '',
description: 'Números para mencionar (separados por vírgula)',
placeholder: '5511999999999,5511888888888',
},
{
displayName: 'Marcar chat como lido',
name: 'readchat',
type: 'boolean',
displayOptions: {
show: {
action: ['media', 'contact']
}
},
default: false,
description: 'Marca conversa como lida após envio',
},
{
displayName: 'Atraso (ms)',
name: 'delay',
type: 'number',
displayOptions: {
show: {
action: ['media', 'contact']
}
},
default: 0,
description: 'Atraso em milissegundos antes do envio, durante o atraso aparecerá "Digitando..."',
},
/* Contato (vCard) */
{
displayName: 'Nome completo',
name: 'fullName',
type: 'string',
displayOptions: { show: { action: ['contact'] } },
default: '',
required: true,
description: 'Nome completo do contato',
},
{
displayName: 'Números de telefone',
name: 'phoneNumber',
type: 'string',
displayOptions: { show: { action: ['contact'] } },
default: '',
required: true,
description: 'Números de telefone (separados por vírgula)',
placeholder: '5511999999999,5511888888888',
},
{
displayName: 'Organização',
name: 'organization',
type: 'string',
displayOptions: { show: { action: ['contact'] } },
default: '',
description: 'Nome da organização/empresa',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: { show: { action: ['contact'] } },
default: '',
description: 'Endereço de email',
placeholder: 'joao@empresa.com',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
displayOptions: { show: { action: ['contact'] } },
default: '',
description: 'URL pessoal ou da empresa',
placeholder: 'https://empresa.com/joao',
},
/* Localização */
{
displayName: 'Nome do local',
name: 'name',
type: 'string',
displayOptions: { show: { action: ['location'] } },
default: '',
description: 'Nome do local',
},
{
displayName: 'Endereço',
name: 'address',
type: 'string',
displayOptions: { show: { action: ['location'] } },
default: '',
description: 'Endereço completo do local',
},
{
displayName: 'Latitude',
name: 'latitude',
type: 'number',
displayOptions: { show: { action: ['location'] } },
default: 0,
required: true,
description: 'Latitude (-90 a 90)',
},
{
displayName: 'Longitude',
name: 'longitude',
type: 'number',
displayOptions: { show: { action: ['location'] } },
default: 0,
required: true,
description: 'Longitude (-180 a 180)',
},
{
displayName: 'Reply ID',
name: 'replyid',
type: 'string',
displayOptions: { show: { action: ['location'] } },
default: '',
description: 'ID da mensagem para responder',
},
{
displayName: 'Menções',
name: 'mentions',
type: 'string',
displayOptions: { show: { action: ['location'] } },
default: '',
description: 'Números para mencionar (separados por vírgula)',
placeholder: '5511999999999,5511888888888',
},
{
displayName: 'Marcar chat como lido',
name: 'readchat',
type: 'boolean',
displayOptions: { show: { action: ['location'] } },
default: false,
description: 'Marca conversa como lida após envio',
},
{
displayName: 'Atraso (ms)',
name: 'delay',
type: 'number',
displayOptions: { show: { action: ['location'] } },
default: 0,
description: 'Atraso em milissegundos antes do envio, durante o atraso aparecerá "Digitando..."',
},
/* Menu */
{
displayName: 'Tipo do menu',
name: 'menuType',
type: 'options',
displayOptions: { show: { action: ['menu'] } },
options: [
{ name: 'Botões', value: 'button' },
{ name: 'Lista', value: 'list' },
{ name: 'Enquete', value: 'poll' },
{ name: 'Carrossel', value: 'carousel' },
],
default: 'button',
required: true,
description: 'Tipo do menu interativo',
},
{
displayName: 'Texto principal',
name: 'menuText',
type: 'string',
displayOptions: { show: { action: ['menu'] } },
default: '',
required: true,
description: 'Texto principal da mensagem (aceita placeholders)',
},
{
displayName: 'Texto do rodapé',
name: 'footerText',
type: 'string',
displayOptions: { show: { action: ['menu'] } },
default: '',
description: 'Texto do rodapé (opcional para botões e listas)',
},
{
displayName: 'Texto do botão principal',
name: 'listButton',
type: 'string',
displayOptions: { show: { action: ['menu'] } },
default: '',
description: 'Texto do botão principal (para listas)',
},
{
displayName: 'Número de opções selecionáveis',
name: 'sen8nasd',
type: 'number',
displayOptions: { show: { action: ['menu'] } },
default: 1,
description: 'Número máximo de opções selecionáveis (para enquetes)',
},
{
displayName: 'Opções',
name: 'choices',
type: 'json',
displayOptions: { show: { action: ['menu'] } },
default: '["Opção 1|id1","Opção 2|id2"]',
required: true,
description: 'Lista de opções. Use [Título] para seções em listas',
},
{
displayName: 'Reply ID',
name: 'replyid',
type: 'string',
displayOptions: { show: { action: ['menu'] } },
default: '',
description: 'ID da mensagem para responder',
},
{
displayName: 'Menções',
name: 'mentions',
type: 'string',
displayOptions: { show: { action: ['menu'] } },
default: '',
description: 'Números para mencionar (separados por vírgula)',
},
{
displayName: 'Marcar chat como lido',
name: 'readchat',
type: 'boolean',
displayOptions: { show: { action: ['menu'] } },
default: false,
description: 'Marca conversa como lida após envio',
},
{
displayName: 'Atraso (ms)',
name: 'delay',
type: 'number',
displayOptions: { show: { action: ['menu'] } },
default: 0,
description: 'Atraso em milissegundos antes do envio',
},
/* Carousel */
{
displayName: 'Texto principal',
name: 'carouselText',
type: 'string',
displayOptions: { show: { action: ['carousel'] } },
default: '',
required: true,
description: 'Texto principal da mensagem',
},
{
displayName: 'Carrossel (JSON)',
name: 'carousel',
type: 'json',
displayOptions: { show: { action: ['carousel'] } },
default: '[{"text":"Texto do cartão","image":"https://exemplo.com/imagem.jpg","buttons":[{"id":"resposta1","text":"Texto do botão","type":"REPLY"}]}]',
required: true,
description: 'Array de cartões do carrossel com text, image e buttons',
},
{
displayName: 'Atraso (ms)',
name: 'delay',
type: 'number',
displayOptions: { show: { action: ['carousel'] } },
default: 0,
description: 'Atraso em milissegundos antes do envio',
},
{
displayName: 'Marcar chat como lido',
name: 'readchat',
type: 'boolean',
displayOptions: { show: { action: ['carousel'] } },
default: false,
description: 'Marca conversa como lida após envio',
},
/* Status */
{
displayName: 'Tipo do status',
name: 'type',
type: 'options',
displayOptions: { show: { action: ['status'] } },
options: [
{ name: 'Texto', value: 'text' },
{ name: 'Imagem', value: 'image' },
{ name: 'Vídeo', value: 'video' },
{ name: 'Áudio', value: 'audio' },
{ name: 'MyAudio', value: 'myaudio' },
{ name: 'PTT', value: 'ptt' },
],
default: 'text',
required: true,
description: 'Tipo do status',
},
{
displayName: 'Texto',
name: 'text',
type: 'string',
displayOptions: { show: { action: ['status'] } },
default: '',
description: 'Texto principal ou legenda',
},
{
displayName: 'Cor de fundo',
name: 'background_color',
type: 'number',
displayOptions: { show: { action: ['status'] } },
default: 1,
description: 'Código da cor de fundo (1 – 19)',
},
{
displayName: 'Fonte',
name: 'font',
type: 'number',
displayOptions: { show: { action: ['status'] } },
default: 0,
description: 'Estilo da fonte (somente quando type = text) • (0 – 8)',
},
{
displayName: 'Arquivo',
name: 'file',
type: 'string',
displayOptions: { show: { action: ['status'] } },
default: '',
description: 'URL ou Base64 do arquivo de mídia',
},
{
displayName: 'Miniatura',
name: 'thumbnail',
type: 'string',
displayOptions: { show: { action: ['status'] } },
default: '',
description: 'URL ou Base64 da miniatura (opcional para vídeos)',
},
{
displayName: 'MIME type',
name: 'mimetype',
type: 'string',
displayOptions: { show: { action: ['status'] } },
default: '',
description: 'MIME type do arquivo (opcional)',
},
],
}; /* end description */
/*────────── EXECUTE ──────────*/
async execute(this: IExecuteFunctions) {
const creds = await this.getCredentials('japiApi');
const items = this.getInputData();
const out = [];
for (let i = 0; i < items.length; i++) {
const action = this.getNodeParameter('action', i) as string;
const endpoint = `/send/${action}`; // mapeia 1:1 com a API
let body: Record<string, any> = {};
/* switch monta o corpo conforme o tipo */
switch (action) {
case 'text':
body = {
number: this.getNodeParameter('phone', i),
text: this.getNodeParameter('text', i),
};
break;
case 'media':
body = {
number: this.getNodeParameter('phone', i),
type: this.getNodeParameter('mediaType', i),
file: this.getNodeParameter('file', i),
text: this.getNodeParameter('caption', i) || undefined,
docName: this.getNodeParameter('docName', i) || undefined,
mimetype: this.getNodeParameter('mimetype', i) || undefined,
replyid: this.getNodeParameter('replyid', i) || undefined,
mentions: this.getNodeParameter('mentions', i) || undefined,
readchat: this.getNodeParameter('readchat', i) as boolean,
delay: this.getNodeParameter('delay', i) as number,
};
break;
case 'contact':
body = {
number: this.getNodeParameter('phone', i),
fullName: this.getNodeParameter('fullName', i),
phoneNumber: this.getNodeParameter('phoneNumber', i),
organization: this.getNodeParameter('organization', i),
email: this.getNodeParameter('email', i),
url: this.getNodeParameter('url', i),
replyid: this.getNodeParameter('replyid', i),
mentions: this.getNodeParameter('mentions', i),
readchat: this.getNodeParameter('readchat', i) as boolean,
delay: this.getNodeParameter('delay', i) as number,
};
break;
case 'location':
body = {
number: this.getNodeParameter('phone', i),
name: this.getNodeParameter('name', i),
address: this.getNodeParameter('address', i),
latitude: this.getNodeParameter('latitude', i),
longitude: this.getNodeParameter('longitude', i),
replyid: this.getNodeParameter('replyid', i),
mentions: this.getNodeParameter('mentions', i),
readchat: this.getNodeParameter('readchat', i) as boolean,
delay: this.getNodeParameter('delay', i) as number,
};
break;
case 'status':
body = {
type: this.getNodeParameter('type', i),
text: this.getNodeParameter('text', i),
background_color: this.getNodeParameter('background_color', i),
font: this.getNodeParameter('font', i),
file: this.getNodeParameter('file', i),
thumbnail: this.getNodeParameter('thumbnail', i),
mimetype: this.getNodeParameter('mimetype', i),
};
break;
case 'menu':
body = {
number: this.getNodeParameter('phone', i),
type: this.getNodeParameter('menuType', i),
text: this.getNodeParameter('menuText', i),
footerText: this.getNodeParameter('footerText', i),
listButton: this.getNodeParameter('listButton', i),
selectableCount: this.getNodeParameter('selectableCount', i),
choices: this.getNodeParameter('choices', i),
replyid: this.getNodeParameter('replyid', i),
mentions: this.getNodeParameter('mentions', i),
readchat: this.getNodeParameter('readchat', i),
delay: this.getNodeParameter('delay', i),
};
break;
case 'carousel':
body = {
number: this.getNodeParameter('phone', i),
text: this.getNodeParameter('carouselText', i),
carousel: this.getNodeParameter('carousel', i),
delay: this.getNodeParameter('delay', i),
readchat: this.getNodeParameter('readchat', i),
};
break;
}
const options: Record<string, any> = {
method: 'POST',
uri: `${creds.baseUrl}${endpoint}`,
headers: { token: creds.apiKey },
body,
json: true,
};
try {
const res = await this.helpers.request(options);
out.push({ json: res });
} catch (err) {
throw new NodeApiError(this.getNode(), err);
}
}
return this.prepareOutputData(out);
}
}