n8n-nodes-manfuservers-wasap-api 0.0.0-stage → 1.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/CHANGELOG.md +21 -0
- package/LICENSE +9 -0
- package/README.md +42 -2
- package/dist/credentials/ManfuserversWasapApi.credentials.d.ts +8 -0
- package/dist/credentials/ManfuserversWasapApi.credentials.js +14 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasap.node.d.ts +10 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasap.node.js +136 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasapChatTrigger.node.d.ts +17 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasapChatTrigger.node.js +111 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasapTrigger.node.d.ts +12 -0
- package/dist/nodes/ManfuserversWasap/ManfuserversWasapTrigger.node.js +91 -0
- package/dist/nodes/ManfuserversWasap/helpers.d.ts +6 -0
- package/dist/nodes/ManfuserversWasap/helpers.js +32 -0
- package/dist/nodes/ManfuserversWasap/manfuservers-wasap.svg +20 -0
- package/package.json +49 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog — n8n-nodes-manfuservers-wasap-api
|
|
2
|
+
|
|
3
|
+
## 1.0.1 — URL por defecto
|
|
4
|
+
- La credencial Manfuservers Wasap API trae por defecto https://manfuservers.com:9000.
|
|
5
|
+
|
|
6
|
+
## 1.0.0 — primera versión pública con el nombre Manfuservers Wasap
|
|
7
|
+
Sustituye al paquete `n8n-nodes-manfuservers-wasap-api` (versión 1.0.0), que queda pendiente de deprecar.
|
|
8
|
+
- Credencial **Manfuservers Wasap API**: URL del servidor + API key de cliente, con prueba de conexión.
|
|
9
|
+
- Nodo **Manfuservers Wasap**: texto, respuesta, imagen, vídeo, audio, nota de voz, documento, descarga multimedia, contactos, grupos, estado de sesión y mensajes.
|
|
10
|
+
- Nodo **Manfuservers Wasap Trigger**: mensajes, imagen, audio, nota de voz, vídeo, documento, grupos y estado de sesión mediante webhook firmado (HMAC-SHA256). Entrega al menos una vez: deduplicar por `messageId`.
|
|
11
|
+
- Remitente con nombre y número cuando están disponibles.
|
|
12
|
+
- Protección de la cuenta incluida en el servidor (espaciado, escritura simulada, topes por hora y día).
|
|
13
|
+
- Icono propio de Manfuservers.
|
|
14
|
+
|
|
15
|
+
### Requisitos del servidor (los gestiona Manfuservers)
|
|
16
|
+
- Clave con permisos `read`, `send`, `media` (archivos) y `events` (trigger); `raw` opcional.
|
|
17
|
+
- La URL de n8n del cliente debe estar autorizada en el servidor.
|
|
18
|
+
|
|
19
|
+
### Estado de verificación
|
|
20
|
+
- Compilación TypeScript sin errores; 4/4 tests del paquete.
|
|
21
|
+
- Carga en n8n 2.18.5 aislado y prueba de credencial. Ver el informe de pruebas en el repositorio.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manfuservers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# n8n-nodes-manfuservers-wasap-api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Manfuservers Wasap** conecta WhatsApp con n8n: mensajes, multimedia, contactos, grupos y eventos entrantes, usando una sola API key por cliente.
|
|
4
|
+
|
|
5
|
+
## Qué necesitas
|
|
6
|
+
1. La **URL del servidor** que te facilite Manfuservers (`https://manfuservers.com:9000`). Sin `/api` ni `/v1`.
|
|
7
|
+
2. Tu **API Key** de cliente. La sesión de WhatsApp queda asociada en el servidor.
|
|
8
|
+
3. Tu n8n con nodos de comunidad habilitados (Settings → Community Nodes).
|
|
9
|
+
|
|
10
|
+
## Instalación
|
|
11
|
+
1. En n8n: Settings → Community Nodes → Install.
|
|
12
|
+
2. Escribe `n8n-nodes-manfuservers-wasap-api` y confirma el aviso de paquete no verificado.
|
|
13
|
+
3. Crea la credencial **Manfuservers Wasap API** con URL y API Key y pulsa *Test*.
|
|
14
|
+
|
|
15
|
+
## Nodos
|
|
16
|
+
- **Manfuservers Wasap**: enviar texto, responder, enviar imagen/vídeo/audio/nota de voz/documento, descargar multimedia, buscar y obtener contactos, listar grupos, estado de la sesión y obtener mensajes.
|
|
17
|
+
- **Manfuservers Wasap Trigger**: recibe mensajes entrantes (texto, imagen, audio, nota de voz, vídeo, documento, grupos, estado de sesión). Elige los tipos de evento en el desplegable.
|
|
18
|
+
|
|
19
|
+
Cada evento de entrada incluye el nombre del remitente y su número cuando WhatsApp lo permite.
|
|
20
|
+
|
|
21
|
+
## Multimedia: recibir y analizar en n8n
|
|
22
|
+
1. **Trigger** → tipo *Imagen*, *Audio*, *Nota de voz* o *Documento*.
|
|
23
|
+
2. **Manfuservers Wasap** → Recurso *Multimedia* → *Descargar multimedia*. Chat e ID del mensaje se rellenan desde el trigger; el archivo sale como binario.
|
|
24
|
+
3. Analiza según el tipo:
|
|
25
|
+
- **Imagen:** un nodo de visión (OpenAI, Gemini o similar) con el binario.
|
|
26
|
+
- **Audio / nota de voz:** transcripción (Whisper u otro servicio de voz a texto).
|
|
27
|
+
- **Documento:** nodo *Extract From File* (PDF, Word, CSV, Excel).
|
|
28
|
+
|
|
29
|
+
## Protección de la cuenta
|
|
30
|
+
Los envíos se espacian automáticamente, muestran «escribiendo…» antes del mensaje y tienen topes por hora y por día. Es transparente: no requiere configuración. Si WhatsApp limita la cuenta, el nodo devuelve un error claro y no reintenta de inmediato.
|
|
31
|
+
|
|
32
|
+
## Límites conocidos
|
|
33
|
+
- Envíos de hasta 32 MB.
|
|
34
|
+
- Un archivo puede caducar en WhatsApp: el nodo informa «ya no disponible».
|
|
35
|
+
- Los eventos llegan al menos una vez: usa `messageId` para evitar duplicados.
|
|
36
|
+
- Los contactos sin nombre ni número vinculado aparecen como «Sin nombre · sin número vinculado».
|
|
37
|
+
|
|
38
|
+
## Actualizar o desinstalar
|
|
39
|
+
- Actualizar: Settings → Community Nodes → buscar el paquete → Update.
|
|
40
|
+
- Desinstalar: Settings → Community Nodes → … → Uninstall. Los workflows que lo usan dejan de tener nodo hasta reinstalar.
|
|
41
|
+
|
|
42
|
+
## Licencia
|
|
43
|
+
MIT. El paquete no incluye el motor de WhatsApp ni el servidor comercial.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ICredentialType, INodeProperties, IAuthenticateGeneric, ICredentialTestRequest } from 'n8n-workflow';
|
|
2
|
+
export declare class ManfuserversWasapApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
properties: INodeProperties[];
|
|
6
|
+
authenticate: IAuthenticateGeneric;
|
|
7
|
+
test: ICredentialTestRequest;
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ManfuserversWasapApi = void 0;
|
|
4
|
+
class ManfuserversWasapApi {
|
|
5
|
+
name = 'manfuserversWasapApi';
|
|
6
|
+
displayName = 'Manfuservers Wasap API';
|
|
7
|
+
properties = [
|
|
8
|
+
{ displayName: 'URL del servidor', name: 'serverUrl', type: 'string', default: 'https://manfuservers.com:9000', placeholder: 'https://manfuservers.com:9000', required: true, description: 'URL pública comercial, sin /api ni /v1; nunca el contenedor ni la clave maestra' },
|
|
9
|
+
{ displayName: 'API Key', name: 'apiKey', type: 'string', typeOptions: { password: true }, default: '', required: true, description: 'Clave comercial de tu cliente. Un carácter es válido, pero inseguro; se recomienda una clave larga' },
|
|
10
|
+
];
|
|
11
|
+
authenticate = { type: 'generic', properties: { headers: { 'X-Api-Key': '={{$credentials.apiKey}}' } } };
|
|
12
|
+
test = { request: { baseURL: '={{$credentials.serverUrl}}', url: '/v1/connection', method: 'GET' } };
|
|
13
|
+
}
|
|
14
|
+
exports.ManfuserversWasapApi = ManfuserversWasapApi;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
|
2
|
+
export declare class ManfuserversWasap implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
listSearch: {
|
|
6
|
+
searchRecipients(this: ILoadOptionsFunctions, filter?: string, paginationToken?: string): Promise<INodeListSearchResult>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ManfuserversWasap = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
const show = (resource, operation) => ({ show: { resource, ...(operation ? { operation } : {}) } });
|
|
6
|
+
const operations = (resource, options, value) => ({ displayName: 'Operación', name: 'operation', type: 'options', options, default: value, displayOptions: show([resource]), noDataExpression: true });
|
|
7
|
+
const sendOps = ['sendText', 'reply', 'sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'];
|
|
8
|
+
class ManfuserversWasap {
|
|
9
|
+
description = {
|
|
10
|
+
displayName: 'Manfuservers Wasap', name: 'manfuserversWasap', icon: 'file:manfuservers-wasap.svg', group: ['transform'], version: 1,
|
|
11
|
+
description: 'Enviar mensajes y archivos, buscar contactos/grupos y descargar multimedia con tu clave comercial',
|
|
12
|
+
defaults: { name: 'Manfuservers Wasap' }, inputs: ['main'], outputs: ['main'],
|
|
13
|
+
credentials: [{ name: 'manfuserversWasapApi', required: true }],
|
|
14
|
+
properties: [
|
|
15
|
+
{ displayName: 'Recurso', name: 'resource', type: 'options', noDataExpression: true, options: [{ name: 'Mensaje', value: 'message' }, { name: 'Multimedia', value: 'media' }, { name: 'Contacto', value: 'contact' }, { name: 'Grupo', value: 'group' }, { name: 'Sesión', value: 'session' }], default: 'message' },
|
|
16
|
+
operations('message', [{ name: 'Enviar texto', value: 'sendText' }, { name: 'Responder a un mensaje', value: 'reply' }, { name: 'Obtener mensajes', value: 'getMessages' }], 'sendText'),
|
|
17
|
+
operations('media', [{ name: 'Enviar imagen', value: 'sendImage' }, { name: 'Enviar vídeo', value: 'sendVideo' }, { name: 'Enviar audio como archivo', value: 'sendAudio' }, { name: 'Enviar nota de voz', value: 'sendVoice' }, { name: 'Enviar documento', value: 'sendDocument' }, { name: 'Descargar multimedia', value: 'download' }], 'sendImage'),
|
|
18
|
+
operations('contact', [{ name: 'Buscar contactos', value: 'searchContacts' }, { name: 'Obtener información', value: 'contactInfo' }], 'searchContacts'),
|
|
19
|
+
operations('group', [{ name: 'Listar o buscar grupos', value: 'listGroups' }], 'listGroups'),
|
|
20
|
+
operations('session', [{ name: 'Consultar estado', value: 'sessionStatus' }], 'sessionStatus'),
|
|
21
|
+
{ displayName: 'Tipo de destinatario', name: 'recipientType', type: 'options', options: [{ name: 'Contacto', value: 'contact' }, { name: 'Grupo', value: 'group' }, { name: 'Número internacional', value: 'phone' }], default: 'contact', displayOptions: { show: { operation: [...sendOps, 'getMessages'] } } },
|
|
22
|
+
{ displayName: 'Destinatario', name: 'recipient', type: 'resourceLocator', default: { mode: 'list', value: '' }, required: true,
|
|
23
|
+
modes: [{ displayName: 'Buscar por nombre, número o ID', name: 'list', type: 'list', typeOptions: { searchListMethod: 'searchRecipients', searchable: true } }, { displayName: 'ID real o expresión', name: 'id', type: 'string', hint: 'ID real de contacto/grupo. Alternativa si el motor no dispone de agenda' }],
|
|
24
|
+
displayOptions: { show: { operation: [...sendOps, 'getMessages'], recipientType: ['contact', 'group'] } } },
|
|
25
|
+
{ displayName: 'Número internacional', name: 'phone', type: 'string', default: '', placeholder: '+34600000000', description: 'Incluye país. Se verifica en el servidor antes de usar el chat real', displayOptions: { show: { operation: [...sendOps, 'getMessages'], recipientType: ['phone'] } } },
|
|
26
|
+
{ displayName: 'Mensaje', name: 'text', type: 'string', typeOptions: { rows: 4 }, default: '', required: true, displayOptions: { show: { operation: ['sendText', 'reply'] } } },
|
|
27
|
+
{ displayName: 'ID del mensaje al que responder', name: 'replyTo', type: 'string', default: '={{$json.messageId}}', required: true, displayOptions: { show: { operation: ['reply'] } } },
|
|
28
|
+
{ displayName: 'Origen del archivo', name: 'fileSource', type: 'options', options: [{ name: 'Binario de otro nodo', value: 'binary' }, { name: 'URL pública directa', value: 'url' }, { name: 'Base64 (avanzado)', value: 'base64' }], default: 'binary', displayOptions: { show: { operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
29
|
+
{ displayName: 'Propiedad binaria', name: 'binaryProperty', type: 'string', default: 'data', required: true, description: 'Ejemplo: data, procedente de Google Drive u otro nodo', displayOptions: { show: { operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'], fileSource: ['binary'] } } },
|
|
30
|
+
{ displayName: 'URL pública del archivo', name: 'fileUrl', type: 'string', default: '', required: true, description: 'Descarga directa pública, sin redirecciones ni direcciones privadas. Para Google Drive usa preferentemente su salida binaria', displayOptions: { show: { fileSource: ['url'], operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
31
|
+
{ displayName: 'Datos Base64', name: 'base64', type: 'string', typeOptions: { rows: 3 }, default: '', required: true, displayOptions: { show: { fileSource: ['base64'], operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
32
|
+
{ displayName: 'Nombre de archivo', name: 'filename', type: 'string', default: 'archivo.bin', required: true, displayOptions: { show: { fileSource: ['url', 'base64'], operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
33
|
+
{ displayName: 'Tipo MIME', name: 'mimeType', type: 'string', default: 'application/octet-stream', required: true, displayOptions: { show: { fileSource: ['url', 'base64'], operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
34
|
+
{ displayName: 'Pie de archivo', name: 'caption', type: 'string', default: '', displayOptions: { show: { operation: ['sendImage', 'sendVideo', 'sendAudio', 'sendVoice', 'sendDocument'] } } },
|
|
35
|
+
{ displayName: 'ID del chat', name: 'chatId', type: 'string', default: '={{$json.chatId}}', required: true, displayOptions: { show: { operation: ['download'] } } },
|
|
36
|
+
{ displayName: 'ID del mensaje o multimedia', name: 'messageId', type: 'string', default: '={{$json.messageId || $json.media.id}}', required: true, displayOptions: { show: { operation: ['download'] } } },
|
|
37
|
+
{ displayName: 'Propiedad binaria de salida', name: 'outputBinaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['download'] } } },
|
|
38
|
+
{ displayName: 'Búsqueda', name: 'search', type: 'string', default: '', description: 'Nombre, número o identificador real', displayOptions: { show: { operation: ['searchContacts', 'listGroups'] } } },
|
|
39
|
+
{ displayName: 'ID real del contacto', name: 'contactId', type: 'string', default: '={{$json.senderId}}', required: true, displayOptions: { show: { operation: ['contactInfo'] } } },
|
|
40
|
+
{ displayName: 'Opciones avanzadas', name: 'options', type: 'collection', placeholder: 'Añadir opción', default: {}, options: [
|
|
41
|
+
{ displayName: 'Límite de resultados', name: 'limit', type: 'number', default: 50, typeOptions: { minValue: 1, maxValue: 100 } },
|
|
42
|
+
{ displayName: 'Desplazamiento de página', name: 'offset', type: 'number', default: 0, typeOptions: { minValue: 0, maxValue: 100000 } },
|
|
43
|
+
{ displayName: 'Datos raw saneados', name: 'raw', type: 'boolean', default: false, description: 'Solo para obtener mensajes; requiere permiso raw' },
|
|
44
|
+
{ displayName: 'Máximo de descarga (MB)', name: 'downloadMaxMB', type: 'number', default: 32, typeOptions: { minValue: 1, maxValue: 100 } },
|
|
45
|
+
] },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
methods = { listSearch: { async searchRecipients(filter, paginationToken) {
|
|
49
|
+
const type = this.getNodeParameter('recipientType');
|
|
50
|
+
const result = await (0, helpers_1.api)(this, 'GET', `/v1/${type === 'group' ? 'groups' : 'contacts'}?search=${encodeURIComponent(filter || '')}&limit=50&offset=${Number(paginationToken || 0)}`);
|
|
51
|
+
return { results: result.items.map((r) => ({ name: r.label, value: r.id })), ...(result.nextOffset !== null ? { paginationToken: String(result.nextOffset) } : {}) };
|
|
52
|
+
} } };
|
|
53
|
+
async execute() {
|
|
54
|
+
const input = this.getInputData();
|
|
55
|
+
const output = [];
|
|
56
|
+
for (let i = 0; i < input.length; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const operation = this.getNodeParameter('operation', i);
|
|
59
|
+
const opts = this.getNodeParameter('options', i, {});
|
|
60
|
+
const limit = Number(opts.limit || 50), offset = Number(opts.offset || 0);
|
|
61
|
+
let result;
|
|
62
|
+
if (operation === 'sessionStatus')
|
|
63
|
+
result = await (0, helpers_1.api)(this, 'GET', '/v1/session');
|
|
64
|
+
else if (operation === 'searchContacts' || operation === 'listGroups')
|
|
65
|
+
result = await (0, helpers_1.api)(this, 'GET', `/v1/${operation === 'listGroups' ? 'groups' : 'contacts'}?search=${encodeURIComponent(this.getNodeParameter('search', i))}&limit=${limit}&offset=${offset}`);
|
|
66
|
+
else if (operation === 'contactInfo')
|
|
67
|
+
result = await (0, helpers_1.api)(this, 'GET', '/v1/contacts/' + encodeURIComponent(this.getNodeParameter('contactId', i)));
|
|
68
|
+
else if (operation === 'download') {
|
|
69
|
+
const chat = this.getNodeParameter('chatId', i), mid = this.getNodeParameter('messageId', i);
|
|
70
|
+
const r = await (0, helpers_1.api)(this, 'GET', '/v1/media/' + encodeURIComponent(chat) + '/' + encodeURIComponent(mid), undefined, { json: false, encoding: 'arraybuffer', returnFullResponse: true, headers: { 'X-Manfuservers-Max-Bytes': String(Number(opts.downloadMaxMB || 32) * 1048576) } });
|
|
71
|
+
const buffer = Buffer.from(r.body);
|
|
72
|
+
const disposition = String(r.headers['content-disposition'] || '');
|
|
73
|
+
const match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
74
|
+
const filename = match ? decodeURIComponent(match[1]) : 'archivo.bin';
|
|
75
|
+
const mime = String(r.headers['content-type'] || 'application/octet-stream').split(';')[0];
|
|
76
|
+
const property = this.getNodeParameter('outputBinaryProperty', i);
|
|
77
|
+
const binary = await this.helpers.prepareBinaryData(buffer, filename, mime);
|
|
78
|
+
output.push({ json: { messageId: mid, chatId: chat, filename, mimeType: mime, size: buffer.length }, binary: { [property]: binary }, pairedItem: { item: i } });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const type = this.getNodeParameter('recipientType', i);
|
|
83
|
+
let chat;
|
|
84
|
+
if (type === 'phone') {
|
|
85
|
+
const verified = await (0, helpers_1.api)(this, 'POST', '/v1/resolve-number', { phone: (0, helpers_1.manualPhone)(this.getNodeParameter('phone', i)) });
|
|
86
|
+
chat = verified.chatId;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const locator = this.getNodeParameter('recipient', i);
|
|
90
|
+
chat = locator.value;
|
|
91
|
+
}
|
|
92
|
+
if (!chat)
|
|
93
|
+
throw new Error('Selecciona un destinatario o introduce su ID real');
|
|
94
|
+
if (operation === 'getMessages')
|
|
95
|
+
result = await (0, helpers_1.api)(this, 'GET', `/v1/messages/${encodeURIComponent(chat)}?limit=${limit}&offset=${offset}&raw=${opts.raw === true}`);
|
|
96
|
+
else if (operation === 'sendText' || operation === 'reply')
|
|
97
|
+
result = await (0, helpers_1.api)(this, 'POST', '/v1/messages/text', { chatId: chat, text: this.getNodeParameter('text', i), ...(operation === 'reply' ? { replyTo: this.getNodeParameter('replyTo', i) } : {}) });
|
|
98
|
+
else {
|
|
99
|
+
const source = this.getNodeParameter('fileSource', i);
|
|
100
|
+
let file;
|
|
101
|
+
if (source === 'binary') {
|
|
102
|
+
const property = this.getNodeParameter('binaryProperty', i);
|
|
103
|
+
const meta = input[i].binary?.[property];
|
|
104
|
+
if (!meta)
|
|
105
|
+
throw new Error('No existe la propiedad binaria indicada');
|
|
106
|
+
const buffer = await this.helpers.getBinaryDataBuffer(i, property);
|
|
107
|
+
if (buffer.length > 33554432)
|
|
108
|
+
throw new Error('Archivo mayor que 32 MB; divide el archivo o configura un flujo específico');
|
|
109
|
+
file = { data: buffer.toString('base64'), filename: meta.fileName || 'archivo.bin', mimetype: meta.mimeType || 'application/octet-stream' };
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
file = { filename: this.getNodeParameter('filename', i), mimetype: this.getNodeParameter('mimeType', i), ...(source === 'url' ? { url: this.getNodeParameter('fileUrl', i) } : { data: this.getNodeParameter('base64', i) }) };
|
|
113
|
+
const kinds = { sendImage: 'image', sendVideo: 'video', sendAudio: 'audio', sendVoice: 'voice', sendDocument: 'document' };
|
|
114
|
+
if (!kinds[operation])
|
|
115
|
+
throw new Error('Operación no compatible');
|
|
116
|
+
result = await (0, helpers_1.api)(this, 'POST', '/v1/messages/' + kinds[operation], { chatId: chat, file, caption: this.getNodeParameter('caption', i) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Pagination remains explicit instead of silently fetching an entire history.
|
|
120
|
+
output.push({ json: result, pairedItem: { item: i } });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const detailRaw = error?.response?.body?.detail ?? error?.cause?.body?.detail ?? error?.description;
|
|
124
|
+
const detail = typeof detailRaw === 'string' ? detailRaw : null;
|
|
125
|
+
const message = detail || 'Operación no completada. Revisa permisos, conexión, sesión y campos; consulta PRUEBAS/LIMITACIONES para capacidades del motor';
|
|
126
|
+
if (this.continueOnFail()) {
|
|
127
|
+
output.push({ json: { error: message }, pairedItem: { item: i } });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
throw new Error('Elemento ' + (i + 1) + ': ' + message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [output];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.ManfuserversWasap = ManfuserversWasap;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription, IHookFunctions, IWebhookFunctions, IWebhookResponseData, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
|
2
|
+
export declare class ManfuserversWasapChatTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
listSearch: {
|
|
6
|
+
searchChats(this: ILoadOptionsFunctions, filter?: string, paginationToken?: string): Promise<INodeListSearchResult>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
webhookMethods: {
|
|
10
|
+
default: {
|
|
11
|
+
checkExists(this: IHookFunctions): Promise<boolean>;
|
|
12
|
+
create(this: IHookFunctions): Promise<boolean>;
|
|
13
|
+
delete(this: IHookFunctions): Promise<boolean>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ManfuserversWasapChatTrigger = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
// Dispara con cada mensaje nuevo de UN chat: recibido o enviado desde tu teléfono.
|
|
6
|
+
class ManfuserversWasapChatTrigger {
|
|
7
|
+
description = {
|
|
8
|
+
displayName: 'Manfuservers Wasap: Mensaje en chat', name: 'manfuserversWasapChatTrigger', icon: 'file:manfuservers-wasap.svg', group: ['trigger'], version: 1,
|
|
9
|
+
description: 'Se dispara con cada mensaje nuevo de un chat concreto, también los que escribes desde tu teléfono',
|
|
10
|
+
defaults: { name: 'Mensaje en chat' }, inputs: [], outputs: ['main'],
|
|
11
|
+
credentials: [{ name: 'manfuserversWasapApi', required: true }],
|
|
12
|
+
webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'wasap-chat' }],
|
|
13
|
+
properties: [
|
|
14
|
+
{ displayName: 'Tipo de chat', name: 'chatType', type: 'options', default: 'contact', options: [
|
|
15
|
+
{ name: 'Contacto', value: 'contact' }, { name: 'Grupo', value: 'group' }, { name: 'Número internacional', value: 'phone' }
|
|
16
|
+
] },
|
|
17
|
+
{ displayName: 'Chat', name: 'chat', type: 'resourceLocator', default: { mode: 'list', value: '' }, required: true,
|
|
18
|
+
modes: [{ displayName: 'Buscar por nombre o número', name: 'list', type: 'list', typeOptions: { searchListMethod: 'searchChats', searchable: true } }],
|
|
19
|
+
displayOptions: { show: { chatType: ['contact', 'group'] } } },
|
|
20
|
+
{ displayName: 'Número internacional', name: 'phone', type: 'string', default: '', placeholder: '+34600000000', required: true,
|
|
21
|
+
description: 'Incluye el país. El servidor verifica el número antes de escuchar el chat', displayOptions: { show: { chatType: ['phone'] } } },
|
|
22
|
+
{ displayName: 'Mensajes a escuchar', name: 'direction', type: 'options', default: 'all', options: [
|
|
23
|
+
{ name: 'Todos (recibidos y enviados)', value: 'all' },
|
|
24
|
+
{ name: 'Solo los que envío yo (desde mi teléfono)', value: 'out' },
|
|
25
|
+
{ name: 'Solo los que recibo', value: 'in' }
|
|
26
|
+
] },
|
|
27
|
+
{ displayName: 'Datos raw saneados', name: 'raw', type: 'boolean', default: false, description: 'Requiere permiso raw de la clave. Añade datos técnicos saneados' },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
methods = { listSearch: { async searchChats(filter, paginationToken) {
|
|
31
|
+
const type = this.getNodeParameter('chatType');
|
|
32
|
+
const result = await (0, helpers_1.api)(this, 'GET', `/v1/${type === 'group' ? 'groups' : 'contacts'}?search=${encodeURIComponent(filter || '')}&limit=50&offset=${Number(paginationToken || 0)}`);
|
|
33
|
+
return { results: result.items.map((r) => ({ name: r.label, value: r.id })), ...(result.nextOffset !== null ? { paginationToken: String(result.nextOffset) } : {}) };
|
|
34
|
+
} } };
|
|
35
|
+
webhookMethods = { default: {
|
|
36
|
+
async checkExists() {
|
|
37
|
+
const data = this.getWorkflowStaticData('node');
|
|
38
|
+
if (!data.subscriptionId)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
return (await (0, helpers_1.api)(this, 'GET', '/v1/subscriptions/' + data.subscriptionId)).active === true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async create() {
|
|
48
|
+
const callback = this.getNodeWebhookUrl('default');
|
|
49
|
+
if (!callback)
|
|
50
|
+
throw new Error('Guarda el workflow para obtener su URL de webhook');
|
|
51
|
+
const type = this.getNodeParameter('chatType');
|
|
52
|
+
let chat;
|
|
53
|
+
if (type === 'phone') {
|
|
54
|
+
const verified = await (0, helpers_1.api)(this, 'POST', '/v1/resolve-number', { phone: (0, helpers_1.manualPhone)(this.getNodeParameter('phone')) });
|
|
55
|
+
chat = verified.chatId;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
chat = this.getNodeParameter('chat').value;
|
|
59
|
+
}
|
|
60
|
+
if (!chat)
|
|
61
|
+
throw new Error('Selecciona el chat que quieres escuchar');
|
|
62
|
+
const credentials = await this.getCredentials('manfuserversWasapApi');
|
|
63
|
+
const secret = (0, helpers_1.hookSecret)(String(credentials.apiKey), String(this.getWorkflow().id || 'new'), this.getNode().id);
|
|
64
|
+
const result = await (0, helpers_1.api)(this, 'POST', '/v1/subscriptions', {
|
|
65
|
+
workflow: String(this.getWorkflow().id || 'new') + ':' + this.getNode().id, callback, secret,
|
|
66
|
+
events: ['message'], raw: this.getNodeParameter('raw'), includeOwn: true,
|
|
67
|
+
chatId: chat, direction: this.getNodeParameter('direction'),
|
|
68
|
+
});
|
|
69
|
+
this.getWorkflowStaticData('node').subscriptionId = result.id;
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
async delete() {
|
|
73
|
+
const data = this.getWorkflowStaticData('node');
|
|
74
|
+
if (data.subscriptionId) {
|
|
75
|
+
try {
|
|
76
|
+
await (0, helpers_1.api)(this, 'DELETE', '/v1/subscriptions/' + data.subscriptionId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Una clave revocada no puede borrar; el servidor ya deja de entregar eventos.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
delete data.subscriptionId;
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
} };
|
|
86
|
+
async webhook() {
|
|
87
|
+
const response = this.getResponseObject();
|
|
88
|
+
try {
|
|
89
|
+
const credentials = await this.getCredentials('manfuserversWasapApi');
|
|
90
|
+
const secret = (0, helpers_1.hookSecret)(String(credentials.apiKey), String(this.getWorkflow().id || 'new'), this.getNode().id);
|
|
91
|
+
const request = this.getRequestObject();
|
|
92
|
+
const signature = String(request.headers['x-manfuservers-signature'] || '');
|
|
93
|
+
const body = this.getBodyData();
|
|
94
|
+
const message = (0, helpers_1.decodeEnvelope)(body, signature, secret);
|
|
95
|
+
const state = this.getWorkflowStaticData('node');
|
|
96
|
+
const previous = Array.isArray(state.deliveryIds) ? state.deliveryIds : [];
|
|
97
|
+
if (previous.includes(String(body.deliveryId))) {
|
|
98
|
+
response.status(200).json({ accepted: true, duplicate: true });
|
|
99
|
+
return { noWebhookResponse: true };
|
|
100
|
+
}
|
|
101
|
+
state.deliveryIds = [...previous, String(body.deliveryId)].slice(-500);
|
|
102
|
+
response.status(200).json({ accepted: true });
|
|
103
|
+
return { noWebhookResponse: true, workflowData: [this.helpers.returnJsonArray(message)] };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
response.status(401).json({ error: 'Firma o evento no válido' });
|
|
107
|
+
return { noWebhookResponse: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.ManfuserversWasapChatTrigger = ManfuserversWasapChatTrigger;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription, IHookFunctions, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
export declare class ManfuserversWasapTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhookMethods: {
|
|
5
|
+
default: {
|
|
6
|
+
checkExists(this: IHookFunctions): Promise<boolean>;
|
|
7
|
+
create(this: IHookFunctions): Promise<boolean>;
|
|
8
|
+
delete(this: IHookFunctions): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ManfuserversWasapTrigger = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
class ManfuserversWasapTrigger {
|
|
6
|
+
description = {
|
|
7
|
+
displayName: 'Manfuservers Wasap Trigger', name: 'manfuserversWasapTrigger', icon: 'file:manfuservers-wasap.svg', group: ['trigger'], version: 1,
|
|
8
|
+
description: 'Recibe mensajes y archivos de tu sesión mediante webhooks firmados, sin polling de WhatsApp',
|
|
9
|
+
defaults: { name: 'Manfuservers Wasap Trigger' }, inputs: [], outputs: ['main'],
|
|
10
|
+
credentials: [{ name: 'manfuserversWasapApi', required: true }],
|
|
11
|
+
webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'wasap' }],
|
|
12
|
+
properties: [
|
|
13
|
+
{ displayName: 'Eventos', name: 'events', type: 'multiOptions', required: true, default: ['message'], options: [
|
|
14
|
+
{ name: 'Cualquier mensaje', value: 'message' }, { name: 'Imagen', value: 'image' }, { name: 'Audio', value: 'audio' },
|
|
15
|
+
{ name: 'Nota de voz', value: 'voice' }, { name: 'Vídeo', value: 'video' }, { name: 'Documento', value: 'document' },
|
|
16
|
+
{ name: 'Cualquier multimedia', value: 'media' }, { name: 'Mensaje en grupo', value: 'group' }, { name: 'Estado de sesión', value: 'session.status' },
|
|
17
|
+
] },
|
|
18
|
+
{ displayName: 'Modo avanzado (raw saneado)', name: 'raw', type: 'boolean', default: false, description: 'Requiere permiso raw. Mantiene la salida simple y añade raw sin claves ni datos de conexión internos' },
|
|
19
|
+
{ displayName: 'Incluir mensajes propios', name: 'includeOwn', type: 'boolean', default: false, description: 'Puede provocar bucles de respuestas. Desactivado por defecto' },
|
|
20
|
+
{ displayName: 'El administrador debe conectar los webhooks nativos y permitir el origen URL de n8n. Activar este workflow registra solo su suscripción; no reemplaza las demás.', name: 'notice', type: 'notice', default: '' },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
webhookMethods = { default: {
|
|
24
|
+
async checkExists() {
|
|
25
|
+
const data = this.getWorkflowStaticData('node');
|
|
26
|
+
if (!data.subscriptionId)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
return (await (0, helpers_1.api)(this, 'GET', '/v1/subscriptions/' + data.subscriptionId)).active === true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async create() {
|
|
36
|
+
const callback = this.getNodeWebhookUrl('default');
|
|
37
|
+
if (!callback)
|
|
38
|
+
throw new Error('Guarda el workflow para obtener su URL de webhook');
|
|
39
|
+
const credentials = await this.getCredentials('manfuserversWasapApi');
|
|
40
|
+
const secret = (0, helpers_1.hookSecret)(String(credentials.apiKey), String(this.getWorkflow().id || 'new'), this.getNode().id);
|
|
41
|
+
const result = await (0, helpers_1.api)(this, 'POST', '/v1/subscriptions', {
|
|
42
|
+
workflow: String(this.getWorkflow().id || 'new') + ':' + this.getNode().id, callback, secret,
|
|
43
|
+
events: this.getNodeParameter('events'), raw: this.getNodeParameter('raw'), includeOwn: this.getNodeParameter('includeOwn'),
|
|
44
|
+
});
|
|
45
|
+
this.getWorkflowStaticData('node').subscriptionId = result.id;
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
async delete() {
|
|
49
|
+
const data = this.getWorkflowStaticData('node');
|
|
50
|
+
if (data.subscriptionId) {
|
|
51
|
+
try {
|
|
52
|
+
await (0, helpers_1.api)(this, 'DELETE', '/v1/subscriptions/' + data.subscriptionId);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A revoked key cannot delete. Backend already blocks delivery; admin can clean orphaned subscriptions.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
delete data.subscriptionId;
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
} };
|
|
62
|
+
async webhook() {
|
|
63
|
+
const response = this.getResponseObject();
|
|
64
|
+
try {
|
|
65
|
+
const credentials = await this.getCredentials('manfuserversWasapApi');
|
|
66
|
+
const secret = (0, helpers_1.hookSecret)(String(credentials.apiKey), String(this.getWorkflow().id || 'new'), this.getNode().id);
|
|
67
|
+
const request = this.getRequestObject();
|
|
68
|
+
const signature = String(request.headers['x-manfuservers-signature'] || '');
|
|
69
|
+
const body = this.getBodyData();
|
|
70
|
+
const message = (0, helpers_1.decodeEnvelope)(body, signature, secret);
|
|
71
|
+
if (message.fromMe && !this.getNodeParameter('includeOwn')) {
|
|
72
|
+
response.status(200).json({ accepted: true, filtered: true });
|
|
73
|
+
return { noWebhookResponse: true };
|
|
74
|
+
}
|
|
75
|
+
const state = this.getWorkflowStaticData('node');
|
|
76
|
+
const previous = Array.isArray(state.deliveryIds) ? state.deliveryIds : [];
|
|
77
|
+
if (previous.includes(String(body.deliveryId))) {
|
|
78
|
+
response.status(200).json({ accepted: true, duplicate: true });
|
|
79
|
+
return { noWebhookResponse: true };
|
|
80
|
+
}
|
|
81
|
+
state.deliveryIds = [...previous, String(body.deliveryId)].slice(-500);
|
|
82
|
+
response.status(200).json({ accepted: true });
|
|
83
|
+
return { noWebhookResponse: true, workflowData: [this.helpers.returnJsonArray(message)] };
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
response.status(401).json({ error: 'Firma o evento webhook no válido' });
|
|
87
|
+
return { noWebhookResponse: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.ManfuserversWasapTrigger = ManfuserversWasapTrigger;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IExecuteFunctions, ILoadOptionsFunctions, IHookFunctions, IWebhookFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
|
2
|
+
export type Context = IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions;
|
|
3
|
+
export declare function api(context: Context, method: string, path: string, body?: unknown, extra?: Partial<IHttpRequestOptions>): Promise<any>;
|
|
4
|
+
export declare function manualPhone(value: string): string;
|
|
5
|
+
export declare function hookSecret(key: string, workflow: string, node: string): string;
|
|
6
|
+
export declare function decodeEnvelope(envelope: any, signature: string, secret: string): any;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.api = api;
|
|
4
|
+
exports.manualPhone = manualPhone;
|
|
5
|
+
exports.hookSecret = hookSecret;
|
|
6
|
+
exports.decodeEnvelope = decodeEnvelope;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
async function api(context, method, path, body, extra = {}) {
|
|
9
|
+
const credentials = await context.getCredentials('manfuserversWasapApi');
|
|
10
|
+
const base = String(credentials.serverUrl).replace(/\/$/, '');
|
|
11
|
+
const url = new URL(base);
|
|
12
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || (url.pathname !== '/' && url.pathname !== ''))
|
|
13
|
+
throw new Error('URL del servidor no válida; usa solo origen y puerto');
|
|
14
|
+
return context.helpers.httpRequestWithAuthentication.call(context, 'manfuserversWasapApi', { method: method, url: base + path, body: body, json: true, timeout: 60000, disableFollowRedirect: true, ...extra });
|
|
15
|
+
}
|
|
16
|
+
function manualPhone(value) {
|
|
17
|
+
const phone = value.replace(/[ ()-]/g, '');
|
|
18
|
+
if (!/^\+?[1-9]\d{6,14}$/.test(phone))
|
|
19
|
+
throw new Error('Introduce un número internacional completo, con código de país. No se adivina el país');
|
|
20
|
+
return phone.replace(/^\+/, '');
|
|
21
|
+
}
|
|
22
|
+
function hookSecret(key, workflow, node) {
|
|
23
|
+
return (0, node_crypto_1.createHmac)('sha256', key).update('manfuservers-wasap:' + workflow + ':' + node).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
function decodeEnvelope(envelope, signature, secret) {
|
|
26
|
+
if (!envelope || typeof envelope.deliveryId !== 'string' || !/^[a-f0-9-]{36}$/i.test(envelope.deliveryId) || typeof envelope.payload !== 'string' || envelope.payload.length > 1500000 || !/^[A-Za-z0-9+/]*={0,2}$/.test(envelope.payload))
|
|
27
|
+
throw new Error('Evento no válido');
|
|
28
|
+
const expected = (0, node_crypto_1.createHmac)('sha256', secret).update(envelope.deliveryId + '.' + envelope.payload).digest('hex');
|
|
29
|
+
if (!/^[a-f0-9]{64}$/i.test(signature) || !(0, node_crypto_1.timingSafeEqual)(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex')))
|
|
30
|
+
throw new Error('Firma webhook no válida');
|
|
31
|
+
return JSON.parse(Buffer.from(envelope.payload, 'base64').toString('utf8'));
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="64" y1="40" x2="432" y2="472" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#A78BFA"/>
|
|
5
|
+
<stop offset="0.55" stop-color="#60A5FA"/>
|
|
6
|
+
<stop offset="1" stop-color="#34D399"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<radialGradient id="glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(256 120) rotate(90) scale(300)">
|
|
9
|
+
<stop stop-color="#FFFFFF" stop-opacity="0.35"/>
|
|
10
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
11
|
+
</radialGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<rect x="24" y="24" width="464" height="464" rx="120" fill="#050510"/>
|
|
14
|
+
<rect x="40" y="40" width="432" height="432" rx="104" fill="url(#bg)"/>
|
|
15
|
+
<rect x="40" y="40" width="432" height="432" rx="104" fill="url(#glow)"/>
|
|
16
|
+
<path d="M176 146H336A56 56 0 0 1 392 202V262A56 56 0 0 1 336 318H240L170 370V318H176A56 56 0 0 1 120 262V202A56 56 0 0 1 176 146Z" fill="#050510"/>
|
|
17
|
+
<circle cx="200" cy="232" r="17" fill="url(#bg)"/>
|
|
18
|
+
<circle cx="256" cy="232" r="17" fill="url(#bg)"/>
|
|
19
|
+
<circle cx="312" cy="232" r="17" fill="url(#bg)"/>
|
|
20
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-manfuservers-wasap-api",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Manfuservers Wasap: WhatsApp para n8n: mensajes, multimedia, contactos, grupos, eventos y escucha de chats",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"n8n-community-node-package",
|
|
8
|
+
"whatsapp",
|
|
9
|
+
"manfuservers",
|
|
10
|
+
"wasap",
|
|
11
|
+
"n8n"
|
|
12
|
+
],
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Manfuservers"
|
|
15
|
+
},
|
|
16
|
+
"main": "dist/nodes/ManfuserversWasap/ManfuserversWasap.node.js",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && node scripts/copy-icons.cjs",
|
|
25
|
+
"test": "node --test tests/*.test.cjs",
|
|
26
|
+
"pack:check": "npm pack --dry-run"
|
|
27
|
+
},
|
|
28
|
+
"n8n": {
|
|
29
|
+
"n8nNodesApiVersion": 1,
|
|
30
|
+
"credentials": [
|
|
31
|
+
"dist/credentials/ManfuserversWasapApi.credentials.js"
|
|
32
|
+
],
|
|
33
|
+
"nodes": [
|
|
34
|
+
"dist/nodes/ManfuserversWasap/ManfuserversWasap.node.js",
|
|
35
|
+
"dist/nodes/ManfuserversWasap/ManfuserversWasapTrigger.node.js",
|
|
36
|
+
"dist/nodes/ManfuserversWasap/ManfuserversWasapChatTrigger.node.js"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"n8n-workflow": "2.18.3",
|
|
41
|
+
"typescript": "5.9.3",
|
|
42
|
+
"@types/node": "24.10.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.16"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
}
|
|
51
|
+
}
|