n8n-nodes-aib 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# n8n-nodes-aib
|
|
2
|
+
|
|
3
|
+
Nós do **Sistema AIB Digital** para o n8n — a experiência do Chatwoot,
|
|
4
|
+
no nosso sistema: conecta uma credencial e escolhe conta e caixa
|
|
5
|
+
dentro do nó.
|
|
6
|
+
|
|
7
|
+
## O que vem
|
|
8
|
+
|
|
9
|
+
- **Credencial "AIB Digital API"** — URL do sistema + chave (`aib_...`).
|
|
10
|
+
Com chave de **agência**, os dropdowns listam todas as contas.
|
|
11
|
+
- **AIB Digital Trigger** — dispara em `mensagem.recebida` (e outros
|
|
12
|
+
eventos escolhíveis). Ao ativar o workflow, registra a URL de
|
|
13
|
+
webhook na conta sozinho; ao desativar, remove.
|
|
14
|
+
- **AIB Digital** (ações) — enviar mensagem, listar conversas/mensagens,
|
|
15
|
+
mover card no funil (dropdown de etapas), pausar/religar IA,
|
|
16
|
+
atribuir atendente, atualizar contato. Habilitado como **ferramenta
|
|
17
|
+
de agente de IA** (`usableAsTool`): plugue direto num AI Agent.
|
|
18
|
+
|
|
19
|
+
## Segurança
|
|
20
|
+
|
|
21
|
+
O gatilho recebe o segredo da conta ao registrar o webhook e confere
|
|
22
|
+
a assinatura `x-aib-signature` (HMAC-SHA256) de cada evento — evento
|
|
23
|
+
com assinatura errada leva 401 e não acorda o fluxo.
|
|
24
|
+
|
|
25
|
+
## Instalar num n8n self-hosted
|
|
26
|
+
|
|
27
|
+
O pacote fica publicado no próprio sistema:
|
|
28
|
+
`https://app.aibdigitall.com/n8n-nodes-aib.tgz`
|
|
29
|
+
|
|
30
|
+
No servidor do n8n:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes
|
|
34
|
+
npm install https://app.aibdigitall.com/n8n-nodes-aib.tgz
|
|
35
|
+
# reinicie o n8n
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(Em Docker, o diretório é o volume de dados do n8n, ex.:
|
|
39
|
+
`/home/node/.n8n/nodes` dentro do container.)
|
|
40
|
+
|
|
41
|
+
## Depois de instalar
|
|
42
|
+
|
|
43
|
+
1. Credentials → **AIB Digital API** → URL + chave de agência
|
|
44
|
+
(Integrações → Chaves de API → "chave de agência").
|
|
45
|
+
2. Novo workflow → **AIB Digital Trigger** → escolha a conta → ative.
|
|
46
|
+
3. A partir daí, cada mensagem recebida daquela conta chega no fluxo;
|
|
47
|
+
responda/mova card com o nó **AIB Digital**.
|
|
48
|
+
|
|
49
|
+
## Atenção
|
|
50
|
+
|
|
51
|
+
- Um destino de webhook **por conta**: ativar dois workflows com
|
|
52
|
+
gatilho para a mesma conta faz o segundo roubar o registro do
|
|
53
|
+
primeiro.
|
|
54
|
+
- Rebuild: `bun install && bunx tsc && npm pack` na pasta do pacote;
|
|
55
|
+
o `.tgz` vai para `public/n8n-nodes-aib.tgz` no app.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AibDigitalApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A credencial do Sistema AIB Digital — o par URL + chave, como era
|
|
6
|
+
* com o Chatwoot. Com uma chave de AGÊNCIA, os nós listam todas as
|
|
7
|
+
* contas e caixas; com chave de conta, operam só aquela conta.
|
|
8
|
+
*/
|
|
9
|
+
class AibDigitalApi {
|
|
10
|
+
name = "aibDigitalApi";
|
|
11
|
+
displayName = "AIB Digital API";
|
|
12
|
+
documentationUrl = "https://app.aibdigitall.com";
|
|
13
|
+
properties = [
|
|
14
|
+
{
|
|
15
|
+
displayName: "URL do sistema",
|
|
16
|
+
name: "baseUrl",
|
|
17
|
+
type: "string",
|
|
18
|
+
default: "https://app.aibdigitall.com",
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
displayName: "Chave de API",
|
|
23
|
+
name: "apiKey",
|
|
24
|
+
type: "string",
|
|
25
|
+
typeOptions: { password: true },
|
|
26
|
+
default: "",
|
|
27
|
+
required: true,
|
|
28
|
+
description: "Gerada em Integrações → Chaves de API. Use uma chave de agência para escolher a conta dentro do nó.",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
authenticate = {
|
|
32
|
+
type: "generic",
|
|
33
|
+
properties: {
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: "=Bearer {{$credentials.apiKey}}",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
test = {
|
|
40
|
+
request: {
|
|
41
|
+
baseURL: "={{$credentials.baseUrl}}",
|
|
42
|
+
url: "/api/v1/ping",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
exports.AibDigitalApi = AibDigitalApi;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AibDigital = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
/** Chama a /api/v1 com a credencial do nó. */
|
|
6
|
+
async function api(ctx, method, path, body, qs) {
|
|
7
|
+
const cred = (await ctx.getCredentials("aibDigitalApi"));
|
|
8
|
+
const base = String(cred.baseUrl).replace(/\/+$/, "");
|
|
9
|
+
return ctx.helpers.httpRequest({
|
|
10
|
+
method,
|
|
11
|
+
url: `${base}${path}`,
|
|
12
|
+
qs,
|
|
13
|
+
body,
|
|
14
|
+
json: true,
|
|
15
|
+
headers: { Authorization: `Bearer ${cred.apiKey}` },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** A conta escolhida no nó (vazio = a conta da própria chave). */
|
|
19
|
+
function contaQs(ctx, i) {
|
|
20
|
+
const conta = ctx.getNodeParameter("conta", i, "");
|
|
21
|
+
return conta ? { conta } : {};
|
|
22
|
+
}
|
|
23
|
+
class AibDigital {
|
|
24
|
+
description = {
|
|
25
|
+
displayName: "AIB Digital",
|
|
26
|
+
name: "aibDigital",
|
|
27
|
+
group: ["output"],
|
|
28
|
+
version: 1,
|
|
29
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
30
|
+
description: "Opera o Sistema AIB Digital: conversas, mensagens e funil",
|
|
31
|
+
defaults: { name: "AIB Digital" },
|
|
32
|
+
// Vira ferramenta de agente de IA no n8n — o mesmo truque do nó
|
|
33
|
+
// do Chatwoot que a Íris usa (chatwootTool).
|
|
34
|
+
usableAsTool: true,
|
|
35
|
+
inputs: ["main"],
|
|
36
|
+
outputs: ["main"],
|
|
37
|
+
credentials: [{ name: "aibDigitalApi", required: true }],
|
|
38
|
+
properties: [
|
|
39
|
+
{
|
|
40
|
+
displayName: "Conta",
|
|
41
|
+
name: "conta",
|
|
42
|
+
type: "options",
|
|
43
|
+
typeOptions: { loadOptionsMethod: "getContas" },
|
|
44
|
+
default: "",
|
|
45
|
+
description: "O cliente. Com chave de agência, escolha aqui; com chave de conta, deixe vazio.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
displayName: "Operação",
|
|
49
|
+
name: "operation",
|
|
50
|
+
type: "options",
|
|
51
|
+
noDataExpression: true,
|
|
52
|
+
default: "enviarMensagem",
|
|
53
|
+
options: [
|
|
54
|
+
{ name: "Enviar mensagem", value: "enviarMensagem", action: "Enviar mensagem de texto" },
|
|
55
|
+
{ name: "Listar conversas", value: "listarConversas", action: "Listar conversas" },
|
|
56
|
+
{ name: "Buscar conversa", value: "buscarConversa", action: "Buscar uma conversa" },
|
|
57
|
+
{ name: "Listar mensagens", value: "listarMensagens", action: "Listar mensagens da conversa" },
|
|
58
|
+
{ name: "Mover etapa (Kanban)", value: "moverEtapa", action: "Mover card no funil" },
|
|
59
|
+
{ name: "Pausar/religar IA", value: "mudarIa", action: "Pausar ou religar a IA na conversa" },
|
|
60
|
+
{ name: "Atribuir atendente", value: "atribuirAtendente", action: "Atribuir a conversa a uma pessoa" },
|
|
61
|
+
{ name: "Atualizar contato", value: "atualizarContato", action: "Atualizar dados do contato" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
// ── parâmetros por operação ─────────────────────────────
|
|
65
|
+
{
|
|
66
|
+
displayName: "Conversa (ID)",
|
|
67
|
+
name: "conversaId",
|
|
68
|
+
type: "string",
|
|
69
|
+
default: "",
|
|
70
|
+
required: true,
|
|
71
|
+
displayOptions: {
|
|
72
|
+
show: { operation: ["enviarMensagem", "buscarConversa", "listarMensagens", "moverEtapa", "mudarIa", "atribuirAtendente"] },
|
|
73
|
+
},
|
|
74
|
+
description: "Vem do gatilho (data.conversa_id) ou de Listar conversas",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
displayName: "Texto",
|
|
78
|
+
name: "conteudo",
|
|
79
|
+
type: "string",
|
|
80
|
+
typeOptions: { rows: 3 },
|
|
81
|
+
default: "",
|
|
82
|
+
required: true,
|
|
83
|
+
displayOptions: { show: { operation: ["enviarMensagem"] } },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
displayName: "Responder à mensagem (ID)",
|
|
87
|
+
name: "citadaId",
|
|
88
|
+
type: "string",
|
|
89
|
+
default: "",
|
|
90
|
+
displayOptions: { show: { operation: ["enviarMensagem"] } },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
displayName: "Etapa",
|
|
94
|
+
name: "etapaId",
|
|
95
|
+
type: "options",
|
|
96
|
+
typeOptions: { loadOptionsMethod: "getEtapas", loadOptionsDependsOn: ["conta"] },
|
|
97
|
+
default: "",
|
|
98
|
+
required: true,
|
|
99
|
+
displayOptions: { show: { operation: ["moverEtapa"] } },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
displayName: "IA ativa",
|
|
103
|
+
name: "iaAtiva",
|
|
104
|
+
type: "boolean",
|
|
105
|
+
default: false,
|
|
106
|
+
displayOptions: { show: { operation: ["mudarIa"] } },
|
|
107
|
+
description: "Desligada = a Íris para de responder nesta conversa (agente-off)",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
displayName: "Atendente (ID do usuário)",
|
|
111
|
+
name: "atendenteId",
|
|
112
|
+
type: "string",
|
|
113
|
+
default: "",
|
|
114
|
+
displayOptions: { show: { operation: ["atribuirAtendente"] } },
|
|
115
|
+
description: "Vazio desatribui. É o 'escalar humano': atribui e, se quiser, pause a IA em outra chamada.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
displayName: "Filtros",
|
|
119
|
+
name: "filtros",
|
|
120
|
+
type: "collection",
|
|
121
|
+
placeholder: "Adicionar filtro",
|
|
122
|
+
default: {},
|
|
123
|
+
displayOptions: { show: { operation: ["listarConversas"] } },
|
|
124
|
+
options: [
|
|
125
|
+
{ displayName: "Telefone", name: "telefone", type: "string", default: "" },
|
|
126
|
+
{ displayName: "Atualizadas desde (ISO)", name: "desde", type: "string", default: "" },
|
|
127
|
+
{
|
|
128
|
+
displayName: "Status",
|
|
129
|
+
name: "status",
|
|
130
|
+
type: "options",
|
|
131
|
+
default: "open",
|
|
132
|
+
options: [
|
|
133
|
+
{ name: "Abertas", value: "open" },
|
|
134
|
+
{ name: "Encerradas", value: "closed" },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{ displayName: "Limite", name: "limite", type: "number", default: 50 },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
displayName: "Contato (ID)",
|
|
142
|
+
name: "contatoId",
|
|
143
|
+
type: "string",
|
|
144
|
+
default: "",
|
|
145
|
+
required: true,
|
|
146
|
+
displayOptions: { show: { operation: ["atualizarContato"] } },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
displayName: "Campos do contato",
|
|
150
|
+
name: "camposContato",
|
|
151
|
+
type: "collection",
|
|
152
|
+
placeholder: "Adicionar campo",
|
|
153
|
+
default: {},
|
|
154
|
+
displayOptions: { show: { operation: ["atualizarContato"] } },
|
|
155
|
+
options: [
|
|
156
|
+
{ displayName: "Nome", name: "nome", type: "string", default: "" },
|
|
157
|
+
{ displayName: "E-mail", name: "email", type: "string", default: "" },
|
|
158
|
+
{ displayName: "Observações", name: "observacoes", type: "string", default: "" },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
methods = {
|
|
164
|
+
loadOptions: {
|
|
165
|
+
async getContas() {
|
|
166
|
+
try {
|
|
167
|
+
const res = await api(this, "GET", "/api/v1/contas");
|
|
168
|
+
const contas = (res?.contas ?? []);
|
|
169
|
+
return [
|
|
170
|
+
{ name: "— A conta da chave —", value: "" },
|
|
171
|
+
...contas.map((c) => ({
|
|
172
|
+
name: ["ativa", "ativo", "active"].includes(c.status) ? c.nome : `${c.nome} (suspensa)`,
|
|
173
|
+
value: c.id,
|
|
174
|
+
})),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Chave de conta não lista contas (403): a única opção é ela mesma.
|
|
179
|
+
return [{ name: "— A conta da chave —", value: "" }];
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
async getEtapas() {
|
|
183
|
+
const conta = this.getNodeParameter("conta", "");
|
|
184
|
+
const res = await api(this, "GET", "/api/v1/funis", undefined, conta ? { conta } : undefined);
|
|
185
|
+
const funis = (res?.funis ?? []);
|
|
186
|
+
return funis.flatMap((f) => f.etapas.map((e) => ({ name: `${f.nome} → ${e.nome}`, value: e.id })));
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
async execute() {
|
|
191
|
+
const items = this.getInputData();
|
|
192
|
+
const out = [];
|
|
193
|
+
const operation = this.getNodeParameter("operation", 0);
|
|
194
|
+
for (let i = 0; i < items.length; i++) {
|
|
195
|
+
const qs = contaQs(this, i);
|
|
196
|
+
let res;
|
|
197
|
+
if (operation === "enviarMensagem") {
|
|
198
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
199
|
+
const conteudo = this.getNodeParameter("conteudo", i);
|
|
200
|
+
const citadaId = this.getNodeParameter("citadaId", i, "");
|
|
201
|
+
res = await api(this, "POST", `/api/v1/conversas/${conversaId}/mensagens`, { conteudo, citada_id: citadaId || null }, qs);
|
|
202
|
+
}
|
|
203
|
+
else if (operation === "listarConversas") {
|
|
204
|
+
const f = this.getNodeParameter("filtros", i, {});
|
|
205
|
+
res = await api(this, "GET", "/api/v1/conversas", undefined, {
|
|
206
|
+
...qs,
|
|
207
|
+
...(f.telefone ? { telefone: f.telefone } : {}),
|
|
208
|
+
...(f.desde ? { desde: f.desde } : {}),
|
|
209
|
+
...(f.status ? { status: f.status } : {}),
|
|
210
|
+
...(f.limite ? { limite: String(f.limite) } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (operation === "buscarConversa") {
|
|
214
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
215
|
+
res = await api(this, "GET", `/api/v1/conversas/${conversaId}`, undefined, qs);
|
|
216
|
+
}
|
|
217
|
+
else if (operation === "listarMensagens") {
|
|
218
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
219
|
+
res = await api(this, "GET", `/api/v1/conversas/${conversaId}/mensagens`, undefined, qs);
|
|
220
|
+
}
|
|
221
|
+
else if (operation === "moverEtapa") {
|
|
222
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
223
|
+
const etapaId = this.getNodeParameter("etapaId", i);
|
|
224
|
+
res = await api(this, "POST", `/api/v1/conversas/${conversaId}/etapa`, { etapa_id: etapaId }, qs);
|
|
225
|
+
}
|
|
226
|
+
else if (operation === "mudarIa") {
|
|
227
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
228
|
+
const iaAtiva = this.getNodeParameter("iaAtiva", i);
|
|
229
|
+
res = await api(this, "PATCH", `/api/v1/conversas/${conversaId}`, { ia_ativa: iaAtiva }, qs);
|
|
230
|
+
}
|
|
231
|
+
else if (operation === "atribuirAtendente") {
|
|
232
|
+
const conversaId = this.getNodeParameter("conversaId", i);
|
|
233
|
+
const atendenteId = this.getNodeParameter("atendenteId", i, "");
|
|
234
|
+
res = await api(this, "PATCH", `/api/v1/conversas/${conversaId}`, { atendente_id: atendenteId || null }, qs);
|
|
235
|
+
}
|
|
236
|
+
else if (operation === "atualizarContato") {
|
|
237
|
+
const contatoId = this.getNodeParameter("contatoId", i);
|
|
238
|
+
const campos = this.getNodeParameter("camposContato", i, {});
|
|
239
|
+
res = await api(this, "PATCH", `/api/v1/contatos/${contatoId}`, campos, qs);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operação desconhecida: ${operation}`);
|
|
243
|
+
}
|
|
244
|
+
out.push({ json: res, pairedItem: { item: i } });
|
|
245
|
+
}
|
|
246
|
+
return [out];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exports.AibDigital = AibDigital;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AibDigitalTrigger = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Gatilho "AIB Digital": acorda quando a conta recebe eventos.
|
|
7
|
+
*
|
|
8
|
+
* Ao ATIVAR o workflow, o nó registra a própria URL de webhook na
|
|
9
|
+
* conta escolhida (PUT /api/v1/webhook); ao desativar, remove. É o
|
|
10
|
+
* mesmo campo da tela de Integrações — o nó é um jeito de preenchê-lo.
|
|
11
|
+
*
|
|
12
|
+
* Um destino por conta: ativar um segundo workflow com gatilho para a
|
|
13
|
+
* MESMA conta rouba o registro do primeiro. É o desenho atual do
|
|
14
|
+
* sistema, não do nó.
|
|
15
|
+
*/
|
|
16
|
+
async function apiHook(ctx, method, path, body, qs) {
|
|
17
|
+
const cred = (await ctx.getCredentials("aibDigitalApi"));
|
|
18
|
+
const base = String(cred.baseUrl).replace(/\/+$/, "");
|
|
19
|
+
return ctx.helpers.httpRequest({
|
|
20
|
+
method,
|
|
21
|
+
url: `${base}${path}`,
|
|
22
|
+
qs,
|
|
23
|
+
body,
|
|
24
|
+
json: true,
|
|
25
|
+
headers: { Authorization: `Bearer ${cred.apiKey}` },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
class AibDigitalTrigger {
|
|
29
|
+
description = {
|
|
30
|
+
displayName: "AIB Digital Trigger",
|
|
31
|
+
name: "aibDigitalTrigger",
|
|
32
|
+
group: ["trigger"],
|
|
33
|
+
version: 1,
|
|
34
|
+
description: "Dispara quando chega mensagem (ou outro evento) numa conta do Sistema AIB",
|
|
35
|
+
defaults: { name: "AIB Digital Trigger" },
|
|
36
|
+
inputs: [],
|
|
37
|
+
outputs: ["main"],
|
|
38
|
+
credentials: [{ name: "aibDigitalApi", required: true }],
|
|
39
|
+
webhooks: [
|
|
40
|
+
{
|
|
41
|
+
name: "default",
|
|
42
|
+
httpMethod: "POST",
|
|
43
|
+
responseMode: "onReceived",
|
|
44
|
+
path: "webhook",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
properties: [
|
|
48
|
+
{
|
|
49
|
+
displayName: "Conta",
|
|
50
|
+
name: "conta",
|
|
51
|
+
type: "options",
|
|
52
|
+
typeOptions: { loadOptionsMethod: "getContas" },
|
|
53
|
+
default: "",
|
|
54
|
+
description: "O cliente cujos eventos disparam este workflow",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
displayName: "Eventos",
|
|
58
|
+
name: "eventos",
|
|
59
|
+
type: "multiOptions",
|
|
60
|
+
default: ["mensagem.recebida"],
|
|
61
|
+
options: [
|
|
62
|
+
{ name: "Mensagem recebida", value: "mensagem.recebida" },
|
|
63
|
+
{ name: "Conversa criada (lead novo)", value: "conversa.criada" },
|
|
64
|
+
{ name: "Mensagem enviada (via API)", value: "mensagem.enviada" },
|
|
65
|
+
{ name: "Card movido no funil", value: "conversa.etapa" },
|
|
66
|
+
{ name: "Conversa atualizada (IA/status)", value: "conversa.atualizada" },
|
|
67
|
+
{ name: "Resposta humana no painel (legado)", value: "message.outbound" },
|
|
68
|
+
],
|
|
69
|
+
description: "Eventos que passam; os demais são descartados sem disparar o fluxo",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
methods = {
|
|
74
|
+
loadOptions: {
|
|
75
|
+
async getContas() {
|
|
76
|
+
try {
|
|
77
|
+
const res = await apiHook(this, "GET", "/api/v1/contas");
|
|
78
|
+
const contas = (res?.contas ?? []);
|
|
79
|
+
return [
|
|
80
|
+
{ name: "— A conta da chave —", value: "" },
|
|
81
|
+
...contas.map((c) => ({ name: c.nome, value: c.id })),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [{ name: "— A conta da chave —", value: "" }];
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
webhookMethods = {
|
|
91
|
+
default: {
|
|
92
|
+
async checkExists() {
|
|
93
|
+
const conta = this.getNodeParameter("conta", "");
|
|
94
|
+
const url = this.getNodeWebhookUrl("default");
|
|
95
|
+
try {
|
|
96
|
+
const res = await apiHook(this, "GET", "/api/v1/webhook", undefined, conta ? { conta } : undefined);
|
|
97
|
+
return res?.url === url;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async create() {
|
|
104
|
+
const conta = this.getNodeParameter("conta", "");
|
|
105
|
+
const url = this.getNodeWebhookUrl("default");
|
|
106
|
+
const res = await apiHook(this, "PUT", "/api/v1/webhook", { url }, conta ? { conta } : undefined);
|
|
107
|
+
// O segredo volta no registro; cada evento chega assinado com ele
|
|
108
|
+
// (x-aib-signature) e o webhook confere antes de acordar o fluxo.
|
|
109
|
+
const dados = this.getWorkflowStaticData("node");
|
|
110
|
+
dados.segredo = res?.segredo ?? null;
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
async delete() {
|
|
114
|
+
const conta = this.getNodeParameter("conta", "");
|
|
115
|
+
await apiHook(this, "DELETE", "/api/v1/webhook", undefined, conta ? { conta } : undefined);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
async webhook() {
|
|
121
|
+
const body = this.getBodyData();
|
|
122
|
+
const eventos = this.getNodeParameter("eventos", []);
|
|
123
|
+
// Assinatura: hmac_sha256(segredo, corpo bruto) no x-aib-signature.
|
|
124
|
+
// Sem segredo guardado (registro antigo), passa — compatibilidade,
|
|
125
|
+
// como no nó do Chatwoot. Com segredo, assinatura errada é 401.
|
|
126
|
+
const segredo = this.getWorkflowStaticData("node").segredo;
|
|
127
|
+
if (segredo) {
|
|
128
|
+
const assinatura = this.getHeaderData()["x-aib-signature"] ?? "";
|
|
129
|
+
const corpo = JSON.stringify(this.getBodyData());
|
|
130
|
+
const esperada = (0, crypto_1.createHmac)("sha256", segredo).update(corpo).digest("hex");
|
|
131
|
+
const a = Buffer.from(assinatura);
|
|
132
|
+
const b = Buffer.from(esperada);
|
|
133
|
+
const valida = a.length === b.length && (0, crypto_1.timingSafeEqual)(a, b);
|
|
134
|
+
if (!valida) {
|
|
135
|
+
// Descarte SILENCIOSO: com responseMode onReceived o n8n já
|
|
136
|
+
// respondeu 200, e tentar responder de novo derrubava a
|
|
137
|
+
// conexão. Não disparar o fluxo é a proteção; não confirmar
|
|
138
|
+
// ao forjador que a assinatura era o problema é bônus.
|
|
139
|
+
return { noWebhookResponse: false };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Evento fora da lista: 200 sem disparar — o sistema não precisa
|
|
143
|
+
// reenviar, e o fluxo não acorda à toa.
|
|
144
|
+
if (eventos.length && body?.event && !eventos.includes(body.event)) {
|
|
145
|
+
return { noWebhookResponse: false };
|
|
146
|
+
}
|
|
147
|
+
return { workflowData: [this.helpers.returnJsonArray([body])] };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.AibDigitalTrigger = AibDigitalTrigger;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-aib",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Nós do Sistema AIB Digital para n8n: gatilho de mensagens do WhatsApp (via AIB Inbox) e ações — enviar mensagem, mover card no funil, pausar IA — escolhendo conta e caixa por dropdown.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "AIB Digital",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"n8n-community-node-package",
|
|
9
|
+
"aib-digital",
|
|
10
|
+
"whatsapp",
|
|
11
|
+
"crm",
|
|
12
|
+
"inbox"
|
|
13
|
+
],
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc"
|
|
20
|
+
},
|
|
21
|
+
"n8n": {
|
|
22
|
+
"n8nNodesApiVersion": 1,
|
|
23
|
+
"credentials": [
|
|
24
|
+
"dist/credentials/AibDigitalApi.credentials.js"
|
|
25
|
+
],
|
|
26
|
+
"nodes": [
|
|
27
|
+
"dist/nodes/AibDigital/AibDigital.node.js",
|
|
28
|
+
"dist/nodes/AibDigital/AibDigitalTrigger.node.js"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"n8n-workflow": "*"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"n8n-workflow": "^1.70.0",
|
|
36
|
+
"typescript": "^5.8.3"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://app.aibdigitall.com"
|
|
39
|
+
}
|