n8n-nodes-wa-gateway 0.1.0

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,38 @@
1
+ # n8n-nodes-wa-gateway
2
+
3
+ Node comunitário do n8n para o [wa-gateway](../../README.md).
4
+
5
+ Dois nodes:
6
+
7
+ | Node | Pra quê |
8
+ |---|---|
9
+ | **wa-gateway** | Enviar texto/imagem/documento/vídeo/áudio/localização/enquete, reagir, encaminhar, criar/gerenciar sessões, grupos, contatos, ler histórico. Aceita **binário do nó anterior** (converte pra base64 sozinho). |
10
+ | **wa-gateway Trigger** | Recebe eventos de uma sessão. Ao ativar o workflow, **registra a URL do webhook na config da sessão automaticamente** (e remove ao desativar). Valida HMAC-SHA256 se você definir um segredo. |
11
+
12
+ ## Instalar
13
+
14
+ **Pelo painel do n8n** (Settings → Community Nodes → Install): `n8n-nodes-wa-gateway`
15
+
16
+ **Manual / dev:**
17
+ ```bash
18
+ cd clients/n8n
19
+ npm install
20
+ npm run build
21
+ # copie a pasta dist para ~/.n8n/custom/ (ou use N8N_CUSTOM_EXTENSIONS)
22
+ ```
23
+
24
+ ## Credencial
25
+
26
+ `wa-gateway API`:
27
+ - **Base URL** — `https://seu-gateway.exemplo.com`
28
+ - **API Key** — a `API_KEY` do `.env` ou uma chave da tabela `api_keys`
29
+
30
+ ## Exemplos
31
+
32
+ **Responder mensagem com IA:**
33
+ `wa-gateway Trigger` (events: `message`) → `AI Agent` → `wa-gateway` (Mensagem · Enviar texto, chatId = `{{$json.payload.chatId}}`, text = `{{$json.output}}`).
34
+
35
+ **Enviar uma imagem que veio de um HTTP Request / Google Drive:**
36
+ node anterior produz binário na propriedade `data` → `wa-gateway` (Mensagem · Enviar imagem, Fonte da mídia = *Binário do nó anterior*).
37
+
38
+ **Anti-ban:** marque *Enfileirar* nos envios pra passar pela fila com pacing por número.
@@ -0,0 +1,9 @@
1
+ import type { ICredentialType, INodeProperties, IAuthenticateGeneric, ICredentialTestRequest } from 'n8n-workflow';
2
+ export declare class WaGatewayApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WaGatewayApi = void 0;
4
+ class WaGatewayApi {
5
+ constructor() {
6
+ this.name = 'waGatewayApi';
7
+ this.displayName = 'wa-gateway API';
8
+ this.documentationUrl = 'https://github.com/fdsriopreto-code/wa-gateway';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Base URL',
12
+ name: 'baseUrl',
13
+ type: 'string',
14
+ default: 'http://localhost:3000',
15
+ placeholder: 'https://seu-gateway.exemplo.com',
16
+ required: true,
17
+ },
18
+ {
19
+ displayName: 'API Key',
20
+ name: 'apiKey',
21
+ type: 'string',
22
+ typeOptions: { password: true },
23
+ default: '',
24
+ required: true,
25
+ description: 'Vai no header X-Api-Key. A master key do .env funciona.',
26
+ },
27
+ ];
28
+ this.authenticate = {
29
+ type: 'generic',
30
+ properties: {
31
+ headers: {
32
+ 'X-Api-Key': '={{$credentials.apiKey}}',
33
+ },
34
+ },
35
+ };
36
+ this.test = {
37
+ request: {
38
+ baseURL: '={{$credentials.baseUrl.replace(/\\/$/, "")}}',
39
+ url: '/api/version',
40
+ },
41
+ };
42
+ }
43
+ }
44
+ exports.WaGatewayApi = WaGatewayApi;
@@ -0,0 +1,6 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ /** node ------------------------------------------------------------------- */
3
+ export declare class WaGateway implements INodeType {
4
+ description: INodeTypeDescription;
5
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
6
+ }
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WaGateway = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ /** helpers ------------------------------------------------------------------ */
6
+ async function mediaToBase64(c) {
7
+ const source = c.get('mediaSource', 'binary');
8
+ if (source === 'base64') {
9
+ return { data: c.get('data'), mimetype: c.get('mimetype') || undefined, filename: c.get('filename') || undefined };
10
+ }
11
+ if (source === 'url') {
12
+ const url = c.get('url');
13
+ const buf = (await c.fn.helpers.httpRequest({ method: 'GET', url, encoding: 'arraybuffer' }));
14
+ return { data: Buffer.from(buf).toString('base64'), mimetype: undefined, filename: c.get('filename') || undefined };
15
+ }
16
+ // binary (default)
17
+ const prop = c.get('binaryProperty', 'data') || 'data';
18
+ const bin = c.fn.helpers.assertBinaryData(c.i, prop);
19
+ const buf = await c.fn.helpers.getBinaryDataBuffer(c.i, prop);
20
+ return { data: buf.toString('base64'), mimetype: bin.mimeType, filename: bin.fileName };
21
+ }
22
+ const S = (c) => encodeURIComponent(c.get('session'));
23
+ const JIDp = (c) => encodeURIComponent(c.get('groupJid'));
24
+ /** operation table -------------------------------------------------------- */
25
+ const OPS = [
26
+ // ---- Session
27
+ { resource: 'session', operation: 'list', method: 'GET', path: () => '/api/sessions' },
28
+ { resource: 'session', operation: 'get', method: 'GET', path: (c) => `/api/sessions/${S(c)}` },
29
+ { resource: 'session', operation: 'create', method: 'POST', path: () => '/api/sessions',
30
+ body: (c) => ({ name: c.get('name'), start: c.get('start', true), config: c.get('config', undefined) }) },
31
+ { resource: 'session', operation: 'start', method: 'POST', path: (c) => `/api/sessions/${S(c)}/start` },
32
+ { resource: 'session', operation: 'stop', method: 'POST', path: (c) => `/api/sessions/${S(c)}/stop` },
33
+ { resource: 'session', operation: 'restart', method: 'POST', path: (c) => `/api/sessions/${S(c)}/restart` },
34
+ { resource: 'session', operation: 'logout', method: 'POST', path: (c) => `/api/sessions/${S(c)}/logout` },
35
+ { resource: 'session', operation: 'qr', method: 'GET', path: (c) => `/api/${S(c)}/auth/qr` },
36
+ { resource: 'session', operation: 'pairCode', method: 'POST', path: (c) => `/api/sessions/${S(c)}/auth/pair-code`,
37
+ body: (c) => ({ phone: String(c.get('phone')).replace(/\D/g, '') }) },
38
+ { resource: 'session', operation: 'me', method: 'GET', path: (c) => `/api/${S(c)}/me` },
39
+ // ---- Message
40
+ { resource: 'message', operation: 'sendText', method: 'POST', path: () => '/api/sendText',
41
+ body: (c) => ({ session: c.get('session'), chatId: c.get('chatId'), text: c.get('text'), linkPreview: c.get('linkPreview', false), ...queue(c) }) },
42
+ { resource: 'message', operation: 'sendLocation', method: 'POST', path: () => '/api/sendLocation',
43
+ body: (c) => ({ session: c.get('session'), chatId: c.get('chatId'), latitude: c.get('latitude'), longitude: c.get('longitude'), name: c.get('name', ''), ...queue(c) }) },
44
+ { resource: 'message', operation: 'sendPoll', method: 'POST', path: () => '/api/sendPoll',
45
+ body: (c) => ({ session: c.get('session'), chatId: c.get('chatId'), name: c.get('pollName'), options: String(c.get('pollOptions')).split('\n').map((x) => x.trim()).filter(Boolean), selectable: c.get('selectable', 1), ...queue(c) }) },
46
+ { resource: 'message', operation: 'react', method: 'POST', path: () => '/api/reaction',
47
+ body: (c) => ({ session: c.get('session'), chatId: c.get('chatId'), messageId: c.get('messageId'), emoji: c.get('emoji', ''), fromMe: c.get('fromMe', false) }) },
48
+ { resource: 'message', operation: 'forward', method: 'POST', path: () => '/api/forwardMessage',
49
+ body: (c) => ({ session: c.get('session'), toChatId: c.get('chatId'), messageId: c.get('messageId') }) },
50
+ // media (sendImage / sendFile / sendVideo / sendAudio) resolvido no execute
51
+ // ---- Group
52
+ { resource: 'group', operation: 'list', method: 'GET', path: (c) => `/api/groups?session=${S(c)}` },
53
+ { resource: 'group', operation: 'get', method: 'GET', path: (c) => `/api/groups/${JIDp(c)}?session=${S(c)}` },
54
+ { resource: 'group', operation: 'create', method: 'POST', path: () => '/api/groups',
55
+ body: (c) => ({ session: c.get('session'), name: c.get('name'), participants: lines(c, 'participants') }) },
56
+ { resource: 'group', operation: 'participants', method: 'POST', path: (c) => `/api/groups/${JIDp(c)}/participants`,
57
+ body: (c) => ({ session: c.get('session'), action: c.get('action'), participants: lines(c, 'participants') }) },
58
+ { resource: 'group', operation: 'setName', method: 'PUT', path: (c) => `/api/groups/${JIDp(c)}/name`,
59
+ body: (c) => ({ session: c.get('session'), name: c.get('name') }) },
60
+ { resource: 'group', operation: 'inviteLink', method: 'GET', path: (c) => `/api/groups/${JIDp(c)}/invite-link?session=${S(c)}` },
61
+ // ---- Contact
62
+ { resource: 'contact', operation: 'check', method: 'GET', path: (c) => `/api/contacts/check?session=${S(c)}&phone=${encodeURIComponent(c.get('phone'))}` },
63
+ { resource: 'contact', operation: 'info', method: 'GET', path: (c) => `/api/contacts/info?session=${S(c)}&jid=${encodeURIComponent(c.get('jid'))}` },
64
+ { resource: 'contact', operation: 'picture', method: 'GET', path: (c) => `/api/contacts/profile-picture?session=${S(c)}&jid=${encodeURIComponent(c.get('jid'))}` },
65
+ // ---- Chat / histórico
66
+ { resource: 'chat', operation: 'list', method: 'GET', path: (c) => `/api/chats?session=${S(c)}&limit=${c.get('limit', 100)}` },
67
+ { resource: 'chat', operation: 'history', method: 'GET', path: (c) => `/api/chats/${encodeURIComponent(c.get('chatId'))}/messages?session=${S(c)}&limit=${c.get('limit', 50)}` },
68
+ ];
69
+ const queue = (c) => (c.get('enqueue', false) ? { enqueue: true, delay: c.get('delay', '') || undefined } : {});
70
+ const lines = (c, n) => String(c.get(n, '')).split('\n').map((x) => x.trim()).filter(Boolean);
71
+ /** node ------------------------------------------------------------------- */
72
+ class WaGateway {
73
+ constructor() {
74
+ this.description = {
75
+ displayName: 'wa-gateway',
76
+ name: 'waGateway',
77
+ icon: 'file:waGateway.svg',
78
+ group: ['output'],
79
+ version: 1,
80
+ subtitle: '={{$parameter["operation"] + " · " + $parameter["resource"]}}',
81
+ description: 'Envia mensagens de WhatsApp e gerencia sessões pelo wa-gateway',
82
+ defaults: { name: 'wa-gateway' },
83
+ inputs: ['main'],
84
+ outputs: ['main'],
85
+ credentials: [{ name: 'waGatewayApi', required: true }],
86
+ properties: [
87
+ {
88
+ displayName: 'Recurso', name: 'resource', type: 'options', noDataExpression: true,
89
+ options: [
90
+ { name: 'Mensagem', value: 'message' },
91
+ { name: 'Sessão', value: 'session' },
92
+ { name: 'Grupo', value: 'group' },
93
+ { name: 'Contato', value: 'contact' },
94
+ { name: 'Conversa (histórico)', value: 'chat' },
95
+ ],
96
+ default: 'message',
97
+ },
98
+ // operations per resource
99
+ opt('message', [
100
+ ['Enviar texto', 'sendText'], ['Enviar imagem', 'sendImage'], ['Enviar documento', 'sendFile'],
101
+ ['Enviar vídeo', 'sendVideo'], ['Enviar áudio', 'sendAudio'], ['Enviar localização', 'sendLocation'],
102
+ ['Enviar enquete', 'sendPoll'], ['Reagir', 'react'], ['Encaminhar', 'forward'],
103
+ ], 'sendText'),
104
+ opt('session', [
105
+ ['Criar', 'create'], ['Listar', 'list'], ['Detalhes', 'get'], ['Iniciar', 'start'], ['Parar', 'stop'],
106
+ ['Reiniciar', 'restart'], ['Deslogar', 'logout'], ['QR (texto)', 'qr'], ['Parear por código', 'pairCode'], ['Meu perfil', 'me'],
107
+ ], 'create'),
108
+ opt('group', [
109
+ ['Listar', 'list'], ['Detalhes', 'get'], ['Criar', 'create'], ['Participantes', 'participants'],
110
+ ['Renomear', 'setName'], ['Link de convite', 'inviteLink'],
111
+ ], 'list'),
112
+ opt('contact', [['Checar número', 'check'], ['Info de perfil', 'info'], ['Foto de perfil', 'picture']], 'check'),
113
+ opt('chat', [['Listar conversas', 'list'], ['Histórico da conversa', 'history']], 'history'),
114
+ // ---- common fields
115
+ str('session', 'Sessão', { required: true, show: { resource: ['message', 'group', 'contact', 'chat'] } }),
116
+ str('session', 'Sessão', { required: true, show: { resource: ['session'], operation: ['get', 'start', 'stop', 'restart', 'logout', 'qr', 'pairCode', 'me'] } }),
117
+ str('chatId', 'Chat ID', { placeholder: '5599999999999@s.whatsapp.net', required: true,
118
+ show: { resource: ['message'], operation: ['sendText', 'sendImage', 'sendFile', 'sendVideo', 'sendAudio', 'sendLocation', 'sendPoll', 'react', 'forward'] } }),
119
+ str('chatId', 'Chat ID', { required: true, show: { resource: ['chat'], operation: ['history'] } }),
120
+ str('text', 'Texto', { typeOptions: { rows: 3 }, required: true, show: { resource: ['message'], operation: ['sendText'] } }),
121
+ bool('linkPreview', 'Preview de link', false, { show: { resource: ['message'], operation: ['sendText'] } }),
122
+ // media
123
+ {
124
+ displayName: 'Fonte da mídia', name: 'mediaSource', type: 'options',
125
+ options: [
126
+ { name: 'Binário do nó anterior', value: 'binary' },
127
+ { name: 'URL pública', value: 'url' },
128
+ { name: 'Base64 / data URI', value: 'base64' },
129
+ ],
130
+ default: 'binary',
131
+ displayOptions: { show: { resource: ['message'], operation: ['sendImage', 'sendFile', 'sendVideo', 'sendAudio'] } },
132
+ },
133
+ str('binaryProperty', 'Propriedade binária', { default: 'data',
134
+ show: { resource: ['message'], operation: ['sendImage', 'sendFile', 'sendVideo', 'sendAudio'], mediaSource: ['binary'] } }),
135
+ str('url', 'URL', { placeholder: 'https://…/arquivo.jpg', required: true,
136
+ show: { resource: ['message'], operation: ['sendImage', 'sendFile', 'sendVideo', 'sendAudio'], mediaSource: ['url'] } }),
137
+ str('data', 'Base64', { typeOptions: { rows: 2 }, required: true,
138
+ show: { resource: ['message'], operation: ['sendImage', 'sendFile', 'sendVideo', 'sendAudio'], mediaSource: ['base64'] } }),
139
+ str('caption', 'Legenda', { show: { resource: ['message'], operation: ['sendImage', 'sendFile', 'sendVideo'] } }),
140
+ str('filename', 'Nome do arquivo', { show: { resource: ['message'], operation: ['sendFile'] } }),
141
+ bool('voice', 'Nota de voz (PTT)', false, { show: { resource: ['message'], operation: ['sendAudio'] } }),
142
+ // location
143
+ num('latitude', 'Latitude', { required: true, show: { resource: ['message'], operation: ['sendLocation'] } }),
144
+ num('longitude', 'Longitude', { required: true, show: { resource: ['message'], operation: ['sendLocation'] } }),
145
+ str('name', 'Nome do lugar', { show: { resource: ['message'], operation: ['sendLocation'] } }),
146
+ // poll
147
+ str('pollName', 'Pergunta', { required: true, show: { resource: ['message'], operation: ['sendPoll'] } }),
148
+ str('pollOptions', 'Opções (uma por linha)', { typeOptions: { rows: 3 }, required: true, show: { resource: ['message'], operation: ['sendPoll'] } }),
149
+ num('selectable', 'Opções selecionáveis', { default: 1, show: { resource: ['message'], operation: ['sendPoll'] } }),
150
+ // react / forward
151
+ str('messageId', 'Message ID', { required: true, show: { resource: ['message'], operation: ['react', 'forward'] } }),
152
+ str('emoji', 'Emoji', { placeholder: '👍 (vazio = remove)', show: { resource: ['message'], operation: ['react'] } }),
153
+ bool('fromMe', 'A mensagem alvo é minha', false, { show: { resource: ['message'], operation: ['react'] } }),
154
+ // enqueue (anti-ban)
155
+ bool('enqueue', 'Enfileirar (pacing anti-ban)', false, { show: { resource: ['message'], operation: ['sendText', 'sendImage', 'sendFile', 'sendVideo', 'sendAudio', 'sendLocation', 'sendPoll'] } }),
156
+ str('delay', 'Delay extra', { placeholder: '30s', show: { resource: ['message'], operation: ['sendText', 'sendImage', 'sendFile', 'sendVideo', 'sendAudio', 'sendLocation', 'sendPoll'], enqueue: [true] } }),
157
+ // session create
158
+ str('name', 'Nome da sessão', { placeholder: 'default', required: true, show: { resource: ['session'], operation: ['create'] } }),
159
+ bool('start', 'Iniciar já', true, { show: { resource: ['session'], operation: ['create'] } }),
160
+ { displayName: 'Config (JSON)', name: 'config', type: 'json', default: '', description: 'webhooks / outbox / rawEvents (opcional)',
161
+ displayOptions: { show: { resource: ['session'], operation: ['create'] } } },
162
+ str('phone', 'Telefone (E.164)', { placeholder: '5599999999999', required: true, show: { resource: ['session'], operation: ['pairCode'] } }),
163
+ // group
164
+ str('groupJid', 'Grupo (JID)', { placeholder: '...@g.us', required: true, show: { resource: ['group'], operation: ['get', 'participants', 'setName', 'inviteLink'] } }),
165
+ str('name', 'Nome', { required: true, show: { resource: ['group'], operation: ['create', 'setName'] } }),
166
+ str('participants', 'Participantes (um JID por linha)', { typeOptions: { rows: 3 }, show: { resource: ['group'], operation: ['create', 'participants'] } }),
167
+ { displayName: 'Ação', name: 'action', type: 'options', options: [
168
+ { name: 'Adicionar', value: 'add' }, { name: 'Remover', value: 'remove' }, { name: 'Promover', value: 'promote' }, { name: 'Rebaixar', value: 'demote' },
169
+ ], default: 'add', displayOptions: { show: { resource: ['group'], operation: ['participants'] } } },
170
+ // contact
171
+ str('phone', 'Telefones (vírgula)', { placeholder: '+5599...,+55...', required: true, show: { resource: ['contact'], operation: ['check'] } }),
172
+ str('jid', 'JID(s) (vírgula)', { placeholder: '...@s.whatsapp.net', required: true, show: { resource: ['contact'], operation: ['info', 'picture'] } }),
173
+ // chat
174
+ num('limit', 'Limite', { default: 50, show: { resource: ['chat'] } }),
175
+ ],
176
+ };
177
+ }
178
+ async execute() {
179
+ const items = this.getInputData();
180
+ const out = [];
181
+ const creds = await this.getCredentials('waGatewayApi');
182
+ const base = String(creds.baseUrl).replace(/\/$/, '');
183
+ for (let i = 0; i < items.length; i++) {
184
+ try {
185
+ const resource = this.getNodeParameter('resource', i);
186
+ const operation = this.getNodeParameter('operation', i);
187
+ const c = { fn: this, i, get: (n, d) => { try {
188
+ return this.getNodeParameter(n, i, d);
189
+ }
190
+ catch {
191
+ return d;
192
+ } } };
193
+ let method = 'POST';
194
+ let path = '';
195
+ let body;
196
+ const mediaOp = ['sendImage', 'sendFile', 'sendVideo', 'sendAudio'];
197
+ if (resource === 'message' && mediaOp.includes(operation)) {
198
+ const m = await mediaToBase64(c);
199
+ path = '/api/' + operation;
200
+ body = {
201
+ session: c.get('session'), chatId: c.get('chatId'),
202
+ data: m.data, mimetype: m.mimetype, filename: c.get('filename', m.filename) || m.filename,
203
+ caption: c.get('caption', '') || undefined,
204
+ voice: operation === 'sendAudio' ? c.get('voice', false) : undefined,
205
+ ...queue(c),
206
+ };
207
+ }
208
+ else {
209
+ const op = OPS.find((o) => o.resource === resource && o.operation === operation);
210
+ if (!op)
211
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `operação não suportada: ${resource}.${operation}`);
212
+ method = op.method;
213
+ path = op.path(c);
214
+ body = op.body ? op.body(c) : undefined;
215
+ }
216
+ const res = await this.helpers.httpRequestWithAuthentication.call(this, 'waGatewayApi', {
217
+ method,
218
+ url: base + path,
219
+ body,
220
+ json: true,
221
+ });
222
+ out.push({ json: (res ?? {}), pairedItem: { item: i } });
223
+ }
224
+ catch (err) {
225
+ if (this.continueOnFail()) {
226
+ out.push({ json: { error: err.message }, pairedItem: { item: i } });
227
+ continue;
228
+ }
229
+ throw err;
230
+ }
231
+ }
232
+ return [out];
233
+ }
234
+ }
235
+ exports.WaGateway = WaGateway;
236
+ /** property builders ---------------------------------------------------- */
237
+ function opt(resource, entries, def) {
238
+ return {
239
+ displayName: 'Operação', name: 'operation', type: 'options', noDataExpression: true,
240
+ options: entries.map(([name, value]) => ({ name, value, action: name })),
241
+ default: def,
242
+ displayOptions: { show: { resource: [resource] } },
243
+ };
244
+ }
245
+ function str(name, displayName, o = {}) {
246
+ return {
247
+ displayName, name, type: 'string',
248
+ default: o.default ?? '', placeholder: o.placeholder, required: o.required,
249
+ typeOptions: o.typeOptions,
250
+ displayOptions: o.show ? { show: o.show } : undefined,
251
+ };
252
+ }
253
+ function num(name, displayName, o = {}) {
254
+ return {
255
+ displayName, name, type: 'number', default: o.default ?? 0, required: o.required,
256
+ displayOptions: o.show ? { show: o.show } : undefined,
257
+ };
258
+ }
259
+ function bool(name, displayName, def, o = {}) {
260
+ return {
261
+ displayName, name, type: 'boolean', default: def,
262
+ displayOptions: o.show ? { show: o.show } : undefined,
263
+ };
264
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <rect width="100" height="100" rx="26" fill="#25d366"/>
3
+ <path d="M50 22c-15.5 0-28 12-28 26.8 0 5.2 1.6 10 4.3 14.1L22 78l15.9-4.1c3.8 2 8.1 3.1 12.6 3.1 15.5 0 28-12 28-26.8S65.5 22 50 22z" fill="#03130a" opacity=".92"/>
4
+ <text x="50" y="60" font-size="30" text-anchor="middle" fill="#25d366" font-family="sans-serif" font-weight="bold">w</text>
5
+ </svg>
@@ -0,0 +1,16 @@
1
+ import type { IHookFunctions, IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
2
+ /**
3
+ * Trigger do wa-gateway: cria um webhook no n8n e o registra automaticamente
4
+ * na config da sessão ao ativar o workflow (e remove ao desativar).
5
+ */
6
+ export declare class WaGatewayTrigger implements INodeType {
7
+ description: INodeTypeDescription;
8
+ webhookMethods: {
9
+ default: {
10
+ checkExists(this: IHookFunctions): Promise<boolean>;
11
+ create(this: IHookFunctions): Promise<boolean>;
12
+ delete(this: IHookFunctions): Promise<boolean>;
13
+ };
14
+ };
15
+ webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
16
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WaGatewayTrigger = void 0;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * Trigger do wa-gateway: cria um webhook no n8n e o registra automaticamente
7
+ * na config da sessão ao ativar o workflow (e remove ao desativar).
8
+ */
9
+ class WaGatewayTrigger {
10
+ constructor() {
11
+ this.description = {
12
+ displayName: 'wa-gateway Trigger',
13
+ name: 'waGatewayTrigger',
14
+ icon: 'file:../WaGateway/waGateway.svg',
15
+ group: ['trigger'],
16
+ version: 1,
17
+ subtitle: '={{$parameter["session"] + " · " + ($parameter["events"] || "*")}}',
18
+ description: 'Recebe eventos de uma sessão do wa-gateway (mensagens, status, grupos…)',
19
+ defaults: { name: 'wa-gateway Trigger' },
20
+ inputs: [],
21
+ outputs: ['main'],
22
+ credentials: [{ name: 'waGatewayApi', required: true }],
23
+ webhooks: [
24
+ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'webhook' },
25
+ ],
26
+ properties: [
27
+ {
28
+ displayName: 'Sessão', name: 'session', type: 'string', default: '', required: true,
29
+ description: 'Nome da sessão que vai disparar o fluxo',
30
+ },
31
+ {
32
+ displayName: 'Eventos', name: 'events', type: 'string', default: 'message',
33
+ placeholder: 'message, session.status, group.update · ou *',
34
+ description: 'Lista separada por vírgula. Aceita curinga: message.*, session.*, *',
35
+ },
36
+ {
37
+ displayName: 'Segredo HMAC', name: 'secret', type: 'string', typeOptions: { password: true }, default: '',
38
+ description: 'Se preenchido: assina o webhook e valida X-Webhook-Signature nas entradas',
39
+ },
40
+ {
41
+ displayName: 'Registrar webhook na sessão automaticamente', name: 'autoRegister', type: 'boolean', default: true,
42
+ description: 'Whether to add/remove esta URL em config.webhooks da sessão ao ativar/desativar',
43
+ },
44
+ ],
45
+ };
46
+ this.webhookMethods = {
47
+ default: {
48
+ async checkExists() {
49
+ if (!this.getNodeParameter('autoRegister', true))
50
+ return true;
51
+ const url = this.getNodeWebhookUrl('default');
52
+ const cfg = await getConfig.call(this);
53
+ return (cfg.webhooks || []).some((w) => w.url === url);
54
+ },
55
+ async create() {
56
+ if (!this.getNodeParameter('autoRegister', true))
57
+ return true;
58
+ const url = this.getNodeWebhookUrl('default');
59
+ const events = splitCsv(this.getNodeParameter('events', 'message'));
60
+ const secret = this.getNodeParameter('secret', '');
61
+ const cfg = await getConfig.call(this);
62
+ cfg.webhooks = (cfg.webhooks || []).filter((w) => w.url !== url);
63
+ const wh = { url, events: events.length ? events : ['*'] };
64
+ if (secret)
65
+ wh.hmac = { secret };
66
+ cfg.webhooks.push(wh);
67
+ await putConfig.call(this, cfg);
68
+ return true;
69
+ },
70
+ async delete() {
71
+ if (!this.getNodeParameter('autoRegister', true))
72
+ return true;
73
+ const url = this.getNodeWebhookUrl('default');
74
+ try {
75
+ const cfg = await getConfig.call(this);
76
+ cfg.webhooks = (cfg.webhooks || []).filter((w) => w.url !== url);
77
+ await putConfig.call(this, cfg);
78
+ }
79
+ catch {
80
+ /* sessão pode nem existir mais */
81
+ }
82
+ return true;
83
+ },
84
+ },
85
+ };
86
+ }
87
+ async webhook() {
88
+ const req = this.getRequestObject();
89
+ const body = req.body;
90
+ const secret = this.getNodeParameter('secret', '');
91
+ if (secret) {
92
+ const sig = req.headers['x-webhook-signature'] || '';
93
+ const raw = typeof req.rawBody === 'string' ? Buffer.from(req.rawBody) : req.rawBody || Buffer.from(JSON.stringify(body));
94
+ const expected = 'sha256=' + (0, crypto_1.createHmac)('sha256', secret).update(raw).digest('hex');
95
+ const a = Buffer.from(sig);
96
+ const b = Buffer.from(expected);
97
+ if (a.length !== b.length || !(0, crypto_1.timingSafeEqual)(a, b)) {
98
+ return { webhookResponse: { statusCode: 401, body: 'assinatura inválida' } };
99
+ }
100
+ }
101
+ // filtro local extra (o gateway já filtra, mas caso a config divirja)
102
+ const evName = body.event || '';
103
+ const wanted = splitCsv(this.getNodeParameter('events', ''));
104
+ if (wanted.length && !wanted.some((p) => matchEvent(p, evName))) {
105
+ return { noWebhookResponse: false, workflowData: [[]] };
106
+ }
107
+ return { workflowData: [this.helpers.returnJsonArray([body])] };
108
+ }
109
+ }
110
+ exports.WaGatewayTrigger = WaGatewayTrigger;
111
+ async function base() {
112
+ const creds = await this.getCredentials('waGatewayApi');
113
+ return String(creds.baseUrl).replace(/\/$/, '');
114
+ }
115
+ async function getConfig() {
116
+ const session = this.getNodeParameter('session');
117
+ const res = (await this.helpers.httpRequestWithAuthentication.call(this, 'waGatewayApi', {
118
+ method: 'GET',
119
+ url: (await base.call(this)) + '/api/sessions/' + encodeURIComponent(session),
120
+ json: true,
121
+ }));
122
+ const cfg = res.config || {};
123
+ return { ...cfg, webhooks: Array.isArray(cfg.webhooks) ? cfg.webhooks : [] };
124
+ }
125
+ async function putConfig(cfg) {
126
+ const session = this.getNodeParameter('session');
127
+ await this.helpers.httpRequestWithAuthentication.call(this, 'waGatewayApi', {
128
+ method: 'PUT',
129
+ url: (await base.call(this)) + '/api/sessions/' + encodeURIComponent(session),
130
+ body: { config: cfg },
131
+ json: true,
132
+ });
133
+ }
134
+ function splitCsv(s) {
135
+ return (s || '').split(',').map((x) => x.trim()).filter(Boolean);
136
+ }
137
+ function matchEvent(pattern, name) {
138
+ if (pattern === '*' || pattern === name)
139
+ return true;
140
+ if (pattern.endsWith('.*'))
141
+ return name.startsWith(pattern.slice(0, -1)) || name === pattern.slice(0, -2);
142
+ return false;
143
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "n8n-nodes-wa-gateway",
3
+ "version": "0.1.0",
4
+ "description": "Node n8n para o wa-gateway — API HTTP multi-sessão de WhatsApp",
5
+ "keywords": ["n8n-community-node-package", "n8n", "whatsapp", "wa-gateway", "whatsapp-api"],
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "fdsriopreto-code"
9
+ },
10
+ "homepage": "https://github.com/fdsriopreto-code/wa-gateway",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fdsriopreto-code/wa-gateway.git",
14
+ "directory": "clients/n8n"
15
+ },
16
+ "engines": {
17
+ "node": ">=18.10"
18
+ },
19
+ "main": "index.js",
20
+ "scripts": {
21
+ "build": "tsc && cp nodes/WaGateway/*.svg dist/nodes/WaGateway/",
22
+ "dev": "tsc --watch",
23
+ "lint": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "files": ["dist"],
27
+ "n8n": {
28
+ "n8nNodesApiVersion": 1,
29
+ "credentials": ["dist/credentials/WaGatewayApi.credentials.js"],
30
+ "nodes": [
31
+ "dist/nodes/WaGateway/WaGateway.node.js",
32
+ "dist/nodes/WaGatewayTrigger/WaGatewayTrigger.node.js"
33
+ ]
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20",
37
+ "n8n-workflow": "*",
38
+ "typescript": "^5.4"
39
+ }
40
+ }