n8n-nodes-manfuservers-wasap-api 1.1.0 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog — n8n-nodes-manfuservers-wasap-api
2
2
 
3
+ ## 1.1.1 — un solo trigger
4
+ Sustituye a la 1.1.0 publicada, que tenía dos triggers. El trigger único cubre todos los chats o un chat concreto, con sentido de mensaje (recibidos, enviados o ambos).
5
+
3
6
  ## 1.0.1 — URL por defecto
4
7
  - La credencial Manfuservers Wasap API trae por defecto https://manfuservers.com:9000.
5
8
 
@@ -7,7 +10,7 @@
7
10
  Sustituye al paquete `n8n-nodes-manfuservers-wasap-api` (versión 1.0.0), que queda pendiente de deprecar.
8
11
  - Credencial **Manfuservers Wasap API**: URL del servidor + API key de cliente, con prueba de conexión.
9
12
  - 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`.
13
+ - Nodo **Manfuservers Wasap Trigger** (único trigger): todos los chats o un chat concreto; recibidos, enviados desde el teléfono o ambos. 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
14
  - Remitente con nombre y número cuando están disponibles.
12
15
  - Protección de la cuenta incluida en el servidor (espaciado, escritura simulada, topes por hora y día).
13
16
  - Icono propio de Manfuservers.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ## Nodos
16
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.
17
+ - **Manfuservers Wasap Trigger**: recibe mensajes. Elige *Todos los chats* o *Un chat concreto* (contacto, grupo o número), y el sentido: solo recibidos, solo los que envías tú (también desde tu teléfono), o todos.
18
18
 
19
19
  Cada evento de entrada incluye el nombre del remitente y su número cuando WhatsApp lo permite.
20
20
 
@@ -1,6 +1,11 @@
1
- import type { INodeType, INodeTypeDescription, IHookFunctions, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
1
+ import type { INodeType, INodeTypeDescription, IHookFunctions, IWebhookFunctions, IWebhookResponseData, ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
2
2
  export declare class ManfuserversWasapTrigger implements INodeType {
3
3
  description: INodeTypeDescription;
4
+ methods: {
5
+ listSearch: {
6
+ searchChats(this: ILoadOptionsFunctions, filter?: string, paginationToken?: string): Promise<INodeListSearchResult>;
7
+ };
8
+ };
4
9
  webhookMethods: {
5
10
  default: {
6
11
  checkExists(this: IHookFunctions): Promise<boolean>;
@@ -2,24 +2,47 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ManfuserversWasapTrigger = void 0;
4
4
  const helpers_1 = require("./helpers");
5
+ // Un único trigger: todos los chats o un chat concreto; recibidos, enviados desde tu teléfono o ambos.
5
6
  class ManfuserversWasapTrigger {
6
7
  description = {
7
8
  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
+ description: 'Recibe mensajes de todos tus chats o de uno concreto, también los que escribes desde tu teléfono',
9
10
  defaults: { name: 'Manfuservers Wasap Trigger' }, inputs: [], outputs: ['main'],
10
11
  credentials: [{ name: 'manfuserversWasapApi', required: true }],
11
12
  webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'wasap' }],
12
13
  properties: [
14
+ { displayName: 'Escuchar', name: 'scope', type: 'options', default: 'all', options: [
15
+ { name: 'Todos los chats', value: 'all' },
16
+ { name: 'Un chat concreto', value: 'chat' }
17
+ ] },
18
+ { displayName: 'Tipo de chat', name: 'chatType', type: 'options', default: 'contact', options: [
19
+ { name: 'Contacto', value: 'contact' }, { name: 'Grupo', value: 'group' }, { name: 'Número internacional', value: 'phone' }
20
+ ],
21
+ displayOptions: { show: { scope: ['chat'] } } },
22
+ { displayName: 'Chat', name: 'chat', type: 'resourceLocator', default: { mode: 'list', value: '' }, required: true,
23
+ modes: [{ displayName: 'Buscar por nombre o número', name: 'list', type: 'list', typeOptions: { searchListMethod: 'searchChats', searchable: true } }],
24
+ displayOptions: { show: { scope: ['chat'], chatType: ['contact', 'group'] } } },
25
+ { displayName: 'Número internacional', name: 'phone', type: 'string', default: '', placeholder: '+34600000000', required: true,
26
+ description: 'Incluye el país. El servidor verifica el número antes de escuchar el chat', displayOptions: { show: { scope: ['chat'], chatType: ['phone'] } } },
27
+ { displayName: 'Mensajes', name: 'direction', type: 'options', default: 'in', options: [
28
+ { name: 'Solo los que recibo', value: 'in' },
29
+ { name: 'Solo los que envío yo (desde mi teléfono)', value: 'out' },
30
+ { name: 'Todos (recibidos y enviados)', value: 'all' }
31
+ ],
32
+ description: 'Los enviados incluyen los que escribes desde tu teléfono' },
13
33
  { displayName: 'Eventos', name: 'events', type: 'multiOptions', required: true, default: ['message'], options: [
14
34
  { name: 'Cualquier mensaje', value: 'message' }, { name: 'Imagen', value: 'image' }, { name: 'Audio', value: 'audio' },
15
35
  { 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' },
36
+ { name: 'Cualquier multimedia', value: 'media' }, { name: 'Mensaje en grupo', value: 'group' }, { name: 'Estado de sesión (solo todos los chats)', value: 'session.status' }
17
37
  ] },
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: '' },
38
+ { displayName: 'Datos raw saneados', name: 'raw', type: 'boolean', default: false, description: 'Requiere permiso raw de la clave. Añade datos técnicos saneados' },
21
39
  ],
22
40
  };
41
+ methods = { listSearch: { async searchChats(filter, paginationToken) {
42
+ const type = this.getNodeParameter('chatType');
43
+ const result = await (0, helpers_1.api)(this, 'GET', `/v1/${type === 'group' ? 'groups' : 'contacts'}?search=${encodeURIComponent(filter || '')}&limit=50&offset=${Number(paginationToken || 0)}`);
44
+ return { results: result.items.map((r) => ({ name: r.label, value: r.id })), ...(result.nextOffset !== null ? { paginationToken: String(result.nextOffset) } : {}) };
45
+ } } };
23
46
  webhookMethods = { default: {
24
47
  async checkExists() {
25
48
  const data = this.getWorkflowStaticData('node');
@@ -36,11 +59,26 @@ class ManfuserversWasapTrigger {
36
59
  const callback = this.getNodeWebhookUrl('default');
37
60
  if (!callback)
38
61
  throw new Error('Guarda el workflow para obtener su URL de webhook');
62
+ let chatId = null;
63
+ if (this.getNodeParameter('scope') === 'chat') {
64
+ const type = this.getNodeParameter('chatType');
65
+ if (type === 'phone') {
66
+ const verified = await (0, helpers_1.api)(this, 'POST', '/v1/resolve-number', { phone: (0, helpers_1.manualPhone)(this.getNodeParameter('phone')) });
67
+ chatId = verified.chatId;
68
+ }
69
+ else {
70
+ chatId = this.getNodeParameter('chat').value;
71
+ }
72
+ if (!chatId)
73
+ throw new Error('Selecciona el chat que quieres escuchar');
74
+ }
75
+ const direction = this.getNodeParameter('direction');
39
76
  const credentials = await this.getCredentials('manfuserversWasapApi');
40
77
  const secret = (0, helpers_1.hookSecret)(String(credentials.apiKey), String(this.getWorkflow().id || 'new'), this.getNode().id);
41
78
  const result = await (0, helpers_1.api)(this, 'POST', '/v1/subscriptions', {
42
79
  workflow: String(this.getWorkflow().id || 'new') + ':' + this.getNode().id, callback, secret,
43
- events: this.getNodeParameter('events'), raw: this.getNodeParameter('raw'), includeOwn: this.getNodeParameter('includeOwn'),
80
+ events: this.getNodeParameter('events'), raw: this.getNodeParameter('raw'),
81
+ includeOwn: direction !== 'in', chatId, direction,
44
82
  });
45
83
  this.getWorkflowStaticData('node').subscriptionId = result.id;
46
84
  return true;
@@ -52,7 +90,7 @@ class ManfuserversWasapTrigger {
52
90
  await (0, helpers_1.api)(this, 'DELETE', '/v1/subscriptions/' + data.subscriptionId);
53
91
  }
54
92
  catch {
55
- // A revoked key cannot delete. Backend already blocks delivery; admin can clean orphaned subscriptions.
93
+ // Una clave revocada no puede borrar; el servidor ya deja de entregar eventos.
56
94
  }
57
95
  }
58
96
  delete data.subscriptionId;
@@ -68,10 +106,6 @@ class ManfuserversWasapTrigger {
68
106
  const signature = String(request.headers['x-manfuservers-signature'] || '');
69
107
  const body = this.getBodyData();
70
108
  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
109
  const state = this.getWorkflowStaticData('node');
76
110
  const previous = Array.isArray(state.deliveryIds) ? state.deliveryIds : [];
77
111
  if (previous.includes(String(body.deliveryId))) {
@@ -83,7 +117,7 @@ class ManfuserversWasapTrigger {
83
117
  return { noWebhookResponse: true, workflowData: [this.helpers.returnJsonArray(message)] };
84
118
  }
85
119
  catch {
86
- response.status(401).json({ error: 'Firma o evento webhook no válido' });
120
+ response.status(401).json({ error: 'Firma o evento no válido' });
87
121
  return { noWebhookResponse: true };
88
122
  }
89
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-manfuservers-wasap-api",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Manfuservers Wasap: WhatsApp para n8n: mensajes, multimedia, contactos, grupos, eventos y escucha de chats",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -32,8 +32,7 @@
32
32
  ],
33
33
  "nodes": [
34
34
  "dist/nodes/ManfuserversWasap/ManfuserversWasap.node.js",
35
- "dist/nodes/ManfuserversWasap/ManfuserversWasapTrigger.node.js",
36
- "dist/nodes/ManfuserversWasap/ManfuserversWasapChatTrigger.node.js"
35
+ "dist/nodes/ManfuserversWasap/ManfuserversWasapTrigger.node.js"
37
36
  ]
38
37
  },
39
38
  "devDependencies": {
@@ -1,17 +0,0 @@
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
- }
@@ -1,111 +0,0 @@
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;