n8n-nodes-zalo-custom 1.0.6 → 1.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # n8n-nodes-zalo-custom
2
2
 
3
- Thư viện n8n nodes tự động hoá Zalo thông qua n8n workflow. Cho phép `Expression Zalo Credential` phù hợp khi quản lý nhiều tài khoản Zalo.
3
+ Thư viện n8n nodes tự động hoá Zalo thông qua n8n workflow. Cho phép **Expression Zalo Credential** phù hợp khi quản lý nhiều tài khoản Zalo.
4
4
 
5
5
  Hoạt động độc lập trong môi trường **n8n của bạn** — **không** dùng API của bên thứ ba hay bất kỳ phụ thuộc ngoại vi nào. Dữ liệu của bạn **luôn được giữ riêng tư và an toàn**.
6
6
 
@@ -19,24 +19,24 @@ Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
19
19
 
20
20
  | 💯 | Tính Năng |
21
21
  |:----:|:--------------------------------------------------|
22
- | 🔑 | _Xác thực & Kết nối_ |
22
+ | 🔑 | **_XÁC THỰC & KẾT NỐI_** |
23
23
  | ✓ | Đăng nhập bằng Mã QR |
24
24
  | ✓✓ | Expression Zalo Credential |
25
25
  | ✓ | Hỗ trợ đăng nhập nhiều tài khoản, proxy |
26
26
  | ✓✓ | Đăng nhập, thông báo qua telegram |
27
- | ⚡️ | _Trigger - Lắng nghe sự kiện_ |
27
+ | ⚡️ | **_TRIGGER_** |
28
28
  | ✓ | Sự kiện tin nhắn mới (lọc theo từ khóa, nhóm) |
29
29
  | ✓ | Sự kiện thu hồi tin nhắn, thả cảm xúc |
30
30
  | ✓ | Sự kiện trong nhóm (tham gia, rời, đổi quyền,...) |
31
31
  | ✓ | Sự kiện về bạn bè (kết bạn, hủy bạn) |
32
- | 💬 | _Gửi & Nhận Tin nhắn_ |
32
+ | 💬 | **_NHẮN TIN_** |
33
33
  | ✓ | Gửi tin nhắn (Văn bản, Ảnh, Sticker) |
34
34
  | ✓ | Trả lời tin nhắn (Quote) |
35
35
  | ✓ | Gắn thẻ (Tag) thành viên trong nhóm |
36
- | | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
36
+ | ✓✓ | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
37
37
  | ✓ | Thu hồi tin nhắn đã gửi |
38
38
  | ✓ | Mô phỏng trạng thái "Đang soạn tin..." |
39
- | 👤 | _Quản Bạn & Người dùng_ |
39
+ | 👤 | **_QUẢN TÀI KHOẢN & BẠN BÈ_** |
40
40
  | ✓ | Gửi / Hủy lời mời kết bạn |
41
41
  | ✓ | Chấp nhận / Từ chối lời mời kết bạn |
42
42
  | ✓ | Hủy kết bạn (xóa bạn) |
@@ -45,7 +45,7 @@ Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
45
45
  | ✓ | Lấy thông tin chi tiết người dùng |
46
46
  | ✓ | Cập nhật thông tin cá nhân (Tên, Giới tính,...) |
47
47
  | ✓ | Chặn / Bỏ chặn người dùng |
48
- | 👥 | _Quản Nhóm_ |
48
+ | 👥 | **_QUẢN NHÓM_** |
49
49
  | ✓ | Tạo nhóm mới |
50
50
  | ✓ | Thêm / Xóa thành viên khỏi nhóm |
51
51
  | ✓ | Giải tán nhóm |
@@ -56,7 +56,7 @@ Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
56
56
  | ✓ | Lấy thông tin chi tiết và thành viên nhóm |
57
57
  | ✓ | Tham gia nhóm bằng link / Rời nhóm |
58
58
  | ✓ | Tạo ghi chú (Note) trong nhóm |
59
- | 🎨 | _Tính năng khác_ |
59
+ | 🎨 | **_KHÁC_** |
60
60
  | ✓ | Tạo bình chọn (Poll) trong nhóm |
61
61
  | ✓ | Quản lý thẻ phân loại (Tag) |
62
62
  | ✓ | Tìm kiếm sticker |
@@ -5,10 +5,12 @@ const n8n_workflow_1 = require("n8n-workflow");
5
5
  const ZaloNodeProperties_1 = require("../shared/ZaloNodeProperties");
6
6
  const zca_js_1 = require("zca-js");
7
7
  const zalo_helper_1 = require("../utils/zalo.helper");
8
+
8
9
  const apiInstances = new Map();
9
10
  const reconnectTimers = new Map();
10
- const messageQueue = new Map();
11
+ const activationCleanupMap = new Map();
11
12
  const threadTypeUser = zca_js_1.ThreadType.User;
13
+
12
14
  class ZaloTrigger {
13
15
  constructor() {
14
16
  this.description = {
@@ -23,14 +25,6 @@ class ZaloTrigger {
23
25
  },
24
26
  inputs: [],
25
27
  outputs: ['main'],
26
- webhooks: [
27
- {
28
- name: 'default',
29
- httpMethod: 'POST',
30
- responseMode: 'onReceived',
31
- path: 'webhook',
32
- },
33
- ],
34
28
  credentials: [...ZaloNodeProperties_1.zaloApiCredential],
35
29
  properties: [
36
30
  ...ZaloNodeProperties_1.zaloSessionProperties,
@@ -39,36 +33,12 @@ class ZaloTrigger {
39
33
  name: 'eventTypes',
40
34
  type: 'multiOptions',
41
35
  options: [
42
- {
43
- name: 'User Messages',
44
- value: 'message_user',
45
- description: 'Lắng nghe tin nhắn từ người dùng',
46
- },
47
- {
48
- name: 'Group Messages',
49
- value: 'message_group',
50
- description: 'Lắng nghe tin nhắn từ nhóm',
51
- },
52
- {
53
- name: 'Reaction',
54
- value: 'reaction',
55
- description: 'Sự kiện thả cảm xúc vào tin nhắn',
56
- },
57
- {
58
- name: 'Undo',
59
- value: 'undo',
60
- description: 'Sự kiện thu hồi tin nhắn',
61
- },
62
- {
63
- name: 'Group Event',
64
- value: 'group_event',
65
- description: 'Sự kiện của nhóm (tham gia, rời đi, thăng cấp..)',
66
- },
67
- {
68
- name: 'Friend Event',
69
- value: 'friend_request',
70
- description: 'Sự kiện có người gửi yêu cầu kết bạn',
71
- },
36
+ { name: 'User Messages', value: 'message_user', description: 'Lắng nghe tin nhắn từ người dùng' },
37
+ { name: 'Group Messages', value: 'message_group', description: 'Lắng nghe tin nhắn từ nhóm' },
38
+ { name: 'Reaction', value: 'reaction', description: 'Sự kiện thả cảm xúc vào tin nhắn' },
39
+ { name: 'Undo', value: 'undo', description: 'Sự kiện thu hồi tin nhắn' },
40
+ { name: 'Group Event', value: 'group_event', description: 'Sự kiện của nhóm (tham gia, rời đi, thăng cấp..)' },
41
+ { name: 'Friend Event', value: 'friend_request', description: 'Sự kiện có người gửi yêu cầu kết bạn' },
72
42
  ],
73
43
  default: ['message_user', 'message_group'],
74
44
  required: true,
@@ -79,31 +49,16 @@ class ZaloTrigger {
79
49
  name: 'threadIdFiltering',
80
50
  type: 'boolean',
81
51
  default: false,
82
- displayOptions: {
83
- show: {
84
- eventTypes: [
85
- 'message_user',
86
- 'message_group',
87
- ],
88
- },
89
- },
52
+ displayOptions: { show: { eventTypes: ['message_user', 'message_group'] } },
90
53
  description: 'Chỉ định ID nhóm nhận sự kiện User/Group Message',
91
54
  },
92
55
  {
93
56
  displayName: 'Allowed Thread IDs',
94
57
  name: 'listenOnlyThreads',
95
58
  type: 'string',
96
- typeOptions: {
97
- textArea: true,
98
- },
59
+ typeOptions: { textArea: true },
99
60
  default: '',
100
- displayOptions: {
101
- show: {
102
- threadIdFiltering: [
103
- true,
104
- ],
105
- },
106
- },
61
+ displayOptions: { show: { threadIdFiltering: [true] } },
107
62
  description: 'Chỉ lắng nghe tin nhắn từ các thread ID này (phân cách bằng dấu phẩy). Nếu để trống, sẽ lắng nghe tất cả.',
108
63
  placeholder: 'threadId1,threadId2,threadId3',
109
64
  },
@@ -111,17 +66,9 @@ class ZaloTrigger {
111
66
  displayName: 'Blocked Thread IDs',
112
67
  name: 'ignoreThreads',
113
68
  type: 'string',
114
- typeOptions: {
115
- textArea: true,
116
- },
69
+ typeOptions: { textArea: true },
117
70
  default: '',
118
- displayOptions: {
119
- show: {
120
- threadIdFiltering: [
121
- true,
122
- ],
123
- },
124
- },
71
+ displayOptions: { show: { threadIdFiltering: [true] } },
125
72
  description: 'Bỏ qua tin nhắn từ các thread ID này (phân cách bằng dấu phẩy).',
126
73
  placeholder: 'threadId4,threadId5',
127
74
  },
@@ -130,31 +77,16 @@ class ZaloTrigger {
130
77
  name: 'keywordFiltering',
131
78
  type: 'boolean',
132
79
  default: false,
133
- displayOptions: {
134
- show: {
135
- eventTypes: [
136
- 'message_user',
137
- 'message_group',
138
- ],
139
- },
140
- },
80
+ displayOptions: { show: { eventTypes: ['message_user', 'message_group'] } },
141
81
  description: 'Lọc tin nhắn theo từ khoá khi nhận sự kiện User/Group Message',
142
82
  },
143
83
  {
144
84
  displayName: 'Listen Only Keywords',
145
85
  name: 'listenOnlyKeywords',
146
86
  type: 'string',
147
- typeOptions: {
148
- textArea: true,
149
- },
87
+ typeOptions: { textArea: true },
150
88
  default: '',
151
- displayOptions: {
152
- show: {
153
- keywordFiltering: [
154
- true,
155
- ],
156
- },
157
- },
89
+ displayOptions: { show: { keywordFiltering: [true] } },
158
90
  description: 'Chỉ lắng nghe tin nhắn chứa ít nhất một trong các từ khóa này (phân cách bằng dấu phẩy). Bỏ qua các từ khóa chỉ có 1 ký tự. Không phân biệt chữ hoa/thường.',
159
91
  placeholder: 'ok, chào, tin tức',
160
92
  },
@@ -162,21 +94,12 @@ class ZaloTrigger {
162
94
  displayName: 'Ignore Keywords',
163
95
  name: 'ignoreKeywords',
164
96
  type: 'string',
165
- typeOptions: {
166
- textArea: true,
167
- },
97
+ typeOptions: { textArea: true },
168
98
  default: '',
169
- displayOptions: {
170
- show: {
171
- keywordFiltering: [
172
- true,
173
- ],
174
- },
175
- },
99
+ displayOptions: { show: { keywordFiltering: [true] } },
176
100
  description: 'Bỏ qua tin nhắn chứa bất kỳ từ khóa nào trong danh sách này (phân cách bằng dấu phẩy). Bỏ qua các từ khóa chỉ có 1 ký tự. Không phân biệt chữ hoa/thường.',
177
101
  placeholder: 'spam, quảng cáo',
178
102
  },
179
-
180
103
  {
181
104
  displayName: 'Self Listen',
182
105
  name: 'selfListen',
@@ -184,16 +107,7 @@ class ZaloTrigger {
184
107
  default: false,
185
108
  required: true,
186
109
  displayOptions: {
187
- show: {
188
- eventTypes: [
189
- 'message_user',
190
- 'message_group',
191
- 'reaction',
192
- 'undo',
193
- 'group_event',
194
- 'friend_request',
195
- ],
196
- },
110
+ show: { eventTypes: ['message_user', 'message_group', 'reaction', 'undo', 'group_event', 'friend_request'] },
197
111
  },
198
112
  description: 'Lắng nghe các sự kiện do chính bạn thực hiện.',
199
113
  },
@@ -202,13 +116,7 @@ class ZaloTrigger {
202
116
  name: 'ignoreSelfEvents',
203
117
  type: 'boolean',
204
118
  default: true,
205
- displayOptions: {
206
- show: {
207
- selfListen: [
208
- true,
209
- ],
210
- },
211
- },
119
+ displayOptions: { show: { selfListen: [true] } },
212
120
  description: 'Self Listen chỉ nhận sự kiện User/Group Message (bỏ qua: Thả icon, Thu hồi, Sự kiện nhóm, Kết bạn)',
213
121
  },
214
122
  {
@@ -217,16 +125,7 @@ class ZaloTrigger {
217
125
  type: 'boolean',
218
126
  default: false,
219
127
  displayOptions: {
220
- show: {
221
- eventTypes: [
222
- 'message_user',
223
- 'message_group',
224
- 'reaction',
225
- 'undo',
226
- 'group_event',
227
- 'friend_request',
228
- ],
229
- },
128
+ show: { eventTypes: ['message_user', 'message_group', 'reaction', 'undo', 'group_event', 'friend_request'] },
230
129
  },
231
130
  description: 'Tiếp tục active workflow kể cả khi có 1 Zalo Trigger lỗi (cần bật ở mọi nút. Hiệu quả khi có nhiều trigger trong workflow)',
232
131
  },
@@ -234,15 +133,9 @@ class ZaloTrigger {
234
133
  displayName: 'Telegram Bot Token',
235
134
  name: 'telegramTokenOverride',
236
135
  type: 'string',
237
- typeOptions: {
238
- password: false,
239
- },
136
+ typeOptions: { password: false },
240
137
  default: '',
241
- displayOptions: {
242
- show: {
243
- continueOnFail: [true],
244
- },
245
- },
138
+ displayOptions: { show: { continueOnFail: [true] } },
246
139
  description: 'Bot token dùng để thông báo khi trigger lỗi (để trống nếu credential đã có sẵn)',
247
140
  },
248
141
  {
@@ -250,360 +143,361 @@ class ZaloTrigger {
250
143
  name: 'telegramChatIdOverride',
251
144
  type: 'string',
252
145
  default: '',
253
- displayOptions: {
254
- show: {
255
- continueOnFail: [true],
256
- },
257
- },
146
+ displayOptions: { show: { continueOnFail: [true] } },
258
147
  description: 'ID nhóm chat dùng để thông báo khi trigger lỗi (để trống nếu credential đã có sẵn)',
259
148
  },
260
149
  ],
261
150
  };
262
- this.webhookMethods = {
263
- default: {
264
- async checkExists() {
265
- const credentials = await this.getCredentials('zaloApi');
266
- const useSession = this.getNodeParameter('useSession', 0, false);
267
- const keyId = useSession ? this.getNodeParameter('connectToId', 0, '') : (credentials ? credentials.userId : '');
268
- const webhookUrl = this.getNodeWebhookUrl('default');
269
- const instanceKey = webhookUrl?.includes('/webhook-test/')
270
- ? `${keyId}-test`
271
- : `${keyId}`;
272
- const isConnected = apiInstances.has(instanceKey);
273
- if (isConnected) {
274
- this.logger.info(`checkExists: ${isConnected ? 'LIVE' : 'NOT LIVE'} : "${instanceKey}"`);
275
- }
151
+ }
276
152
 
277
- return isConnected;
278
- },
279
- async create() {
280
- const credentials = await this.getCredentials('zaloApi');
281
- const useSession = this.getNodeParameter('useSession', 0, false);
282
- const keyId = useSession ? this.getNodeParameter('connectToId', 0, '') : (credentials ? credentials.userId : '');
283
- let currentUserId = credentials ? credentials.userId : 'N/A';
284
- let currentName = credentials ? credentials.nameAccount : 'N/A';
285
- let currentPhone = credentials ? credentials.phoneNumber : 'N/A';
286
- const mode = this.getMode(); // 'trigger' or 'manual'
287
- const selfListen = this.getNodeParameter('selfListen', 0, false);
288
- const webhookUrl = this.getNodeWebhookUrl('default');
289
-
290
- const instanceKey = webhookUrl?.includes('/webhook-test/')
291
- ? `${keyId}-test`
292
- : `${keyId}`;
293
- const productionKey = `${keyId}`;
294
- const testKey = `${keyId}-test`;
295
-
296
- if (webhookUrl?.includes('/webhook-test/') && apiInstances.has(productionKey)) {
297
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo trigger: Please deactivate the workflow before testing`);
298
- }
299
- if (!webhookUrl?.includes('/webhook-test/') && apiInstances.has(testKey)) {
300
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo trigger: Please turn off the test listening then try Active Workflow`);
301
- }
153
+ async trigger() {
154
+ const credentials = await this.getCredentials('zaloApi');
155
+ const useSession = this.getNodeParameter('useSession', 0, false);
156
+ const keyId = useSession ? this.getNodeParameter('connectToId', 0, '') : (credentials ? credentials.userId : '');
302
157
 
303
- if (apiInstances.has(instanceKey)) {
304
- this.logger.info(`Instance ${instanceKey} already exists, reusing...`);
305
- const webhookData = this.getWorkflowStaticData('node');
306
- webhookData[instanceKey] = {
307
- isConnected: true,
308
- eventTypes: this.getNodeParameter('eventTypes', 0),
309
- createdAt: new Date().toISOString()
310
- };
311
- return true;
312
- }
158
+ let currentUserId = credentials ? credentials.userId : 'N/A';
159
+ let currentName = credentials ? credentials.nameAccount : 'N/A';
160
+ let currentPhone = credentials ? credentials.phoneNumber : 'N/A';
313
161
 
162
+ const mode = this.getMode(); // 'trigger' or 'manual'
163
+ const selfListen = this.getNodeParameter('selfListen', 0, false);
314
164
 
315
- try {
316
- const api = await (0, zalo_helper_1.getZaloApiClient)(this, { selfListen });
317
- if (!api) {
318
- throw new error('No API instance found. Please make sure to provide valid credentials.')
319
- }
320
- try {
321
- const fetchedInfo = await api.fetchAccountInfo();
322
- if (fetchedInfo && fetchedInfo.profile) {
323
- const newUserId = fetchedInfo.profile.userId;
324
- const newRawPhone = fetchedInfo.profile.phoneNumber || '';
325
- const newPhone = newRawPhone.replace(/^(\+84)/, '0');
326
- const newName = fetchedInfo.profile.zaloName;
327
- currentUserId = newUserId;
328
- currentName = newName;
329
- currentPhone = newPhone;
330
- const hasNameChanged = credentials && newName && newName !== credentials.nameAccount;
331
- const hasPhoneChanged = credentials && newPhone && newPhone !== credentials.phoneNumber;
332
- if (newUserId && (hasNameChanged || hasPhoneChanged)) {
333
- try {
334
- await (0, zalo_helper_1.updateSessionInfo)(newUserId, newName, newPhone);
335
- // this.logger.info(`Account info for ${newUserId} has changed: database updated session.`);
336
- } catch (dbError) {
337
- this.logger.error(`[Zalo ${mode}] Failed to update user: ${newUserId}: ${dbError.message}`);
338
- }
339
- }
165
+ const productionKey = `${keyId}`;
166
+ const manualKey = `${keyId}-manual`;
167
+ const instanceKey = mode === 'manual' ? manualKey : productionKey;
168
+
169
+ if (mode === 'manual' && apiInstances.has(productionKey)) {
170
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo trigger: Please deactivate the workflow before testing`);
171
+ }
172
+ if (mode !== 'manual' && apiInstances.has(manualKey)) {
173
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo trigger: Please turn off the test listening then try Active Workflow`);
174
+ }
175
+
176
+ const emitEvent = (payload) => {
177
+ this.emit([this.helpers.returnJsonArray([{ json: JSON.parse(JSON.stringify(payload)) }])]);
178
+ };
179
+
180
+ const context = {
181
+ instanceKey,
182
+ workflowId: this.getWorkflow().id,
183
+ credentialName: currentName,
184
+ credentialPhone: currentPhone,
185
+ credentialUserId: currentUserId,
186
+ eventTypes: this.getNodeParameter('eventTypes', 0),
187
+ threadIdFiltering: this.getNodeParameter('threadIdFiltering', 0, false),
188
+ keywordFiltering: this.getNodeParameter('keywordFiltering', 0, false),
189
+ ignoreSelfEvents: this.getNodeParameter('ignoreSelfEvents', 0, true),
190
+ listenOnlyThreads: (this.getNodeParameter('listenOnlyThreads', 0, '') || '').split(',').map(id => id.trim()).filter(Boolean),
191
+ ignoreThreads: (this.getNodeParameter('ignoreThreads', 0, '') || '').split(',').map(id => id.trim()).filter(Boolean),
192
+ listenOnlyKeywords: (this.getNodeParameter('listenOnlyKeywords', 0, '') || '').split(',')
193
+ .map(k => k.trim().toLowerCase()).filter(k => k && k.length > 1),
194
+ ignoreKeywords: (this.getNodeParameter('ignoreKeywords', 0, '') || '').split(',')
195
+ .map(k => k.trim().toLowerCase()).filter(k => k && k.length > 1),
196
+ logger: this.logger,
197
+ mode,
198
+ };
199
+
200
+ if (apiInstances.has(instanceKey)) {
201
+ this.logger.info(`Instance ${instanceKey} already exists, reusing...`);
202
+ }
203
+
204
+ let api = apiInstances.get(instanceKey);
205
+ let closeFunction = async () => { };
206
+
207
+ try {
208
+ if (!api) {
209
+ api = await (0, zalo_helper_1.getZaloApiClient)(this, { selfListen });
210
+ if (!api) {
211
+ throw new Error('No API instance found. Please make sure to provide valid credentials.');
212
+ }
213
+
214
+ try {
215
+ const fetchedInfo = await api.fetchAccountInfo();
216
+ if (fetchedInfo && fetchedInfo.profile) {
217
+ const newUserId = fetchedInfo.profile.userId;
218
+ const newRawPhone = fetchedInfo.profile.phoneNumber || '';
219
+ const newPhone = newRawPhone.replace(/^(\+84)/, '0');
220
+ const newName = fetchedInfo.profile.zaloName;
221
+
222
+ currentUserId = newUserId || currentUserId;
223
+ currentName = newName || currentName;
224
+ currentPhone = newPhone || currentPhone;
225
+
226
+ context.credentialUserId = currentUserId;
227
+ context.credentialName = currentName;
228
+ context.credentialPhone = currentPhone;
229
+
230
+ const hasNameChanged = credentials && newName && newName !== credentials.nameAccount;
231
+ const hasPhoneChanged = credentials && newPhone && newPhone !== credentials.phoneNumber;
232
+ if (newUserId && (hasNameChanged || hasPhoneChanged)) {
233
+ try {
234
+ await (0, zalo_helper_1.updateSessionInfo)(newUserId, newName, newPhone);
340
235
  }
341
- else {
342
- this.logger.warn(`[Zalo ${mode}] Could not fetch complete account info for ${instanceKey}.`);
236
+ catch (dbError) {
237
+ this.logger.error(`[Zalo ${mode}] Failed to update user: ${newUserId}: ${dbError.message}`);
343
238
  }
344
239
  }
345
- catch (error) {
346
- this.logger.error(`[Zalo ${mode}] Failed to fetch account info: ${error.message}`);
347
- }
348
- apiInstances.set(instanceKey, api);
349
- // The context object will be stored with the API instance
350
- const context = {
351
- instanceKey,
352
- workflowId: this.getWorkflow().id,
353
- credentialName: currentName,
354
- credentialPhone: currentPhone,
355
- credentialUserId: currentUserId,
356
- eventTypes: this.getNodeParameter('eventTypes', 0),
357
- threadIdFiltering: this.getNodeParameter('threadIdFiltering', 0, false),
358
- keywordFiltering: this.getNodeParameter('keywordFiltering', 0, false),
359
- ignoreSelfEvents: this.getNodeParameter('ignoreSelfEvents', 0, true),
360
- listenOnlyThreads: (this.getNodeParameter('listenOnlyThreads', 0, '') || '').split(',')
361
- .map(id => id.trim())
362
- .filter(id => id),
363
- ignoreThreads: (this.getNodeParameter('ignoreThreads', 0, '') || '').split(',')
364
- .map(id => id.trim())
365
- .filter(id => id),
366
- listenOnlyKeywords: (this.getNodeParameter('listenOnlyKeywords', 0, '') || '').split(',')
367
- .map(keyword => keyword.trim().toLowerCase())
368
- .filter(keyword => keyword && keyword.length > 1), // single-character
369
- ignoreKeywords: (this.getNodeParameter('ignoreKeywords', 0, '') || '').split(',')
370
- .map(keyword => keyword.trim().toLowerCase())
371
- .filter(keyword => keyword && keyword.length > 1), // single-character
372
- webhookUrl: webhookUrl,
373
- logger: this.logger,
374
- helpers: this.helpers,
375
- getWorkflowStaticData: this.getWorkflowStaticData.bind(this),
376
- mode,
377
- };
240
+ }
241
+ else {
242
+ this.logger.warn(`[Zalo ${mode}] Could not fetch complete account info for ${instanceKey}.`);
243
+ }
244
+ }
245
+ catch (err) {
246
+ this.logger.error(`[Zalo ${mode}] Failed to fetch account info: ${err.message}`);
247
+ }
378
248
 
379
- if (context.eventTypes.includes('message_user') || context.eventTypes.includes('message_group')) {
380
- api.listener.on('message', async (message) => {
381
- const eventType = message.type === threadTypeUser ? 'message_user' : 'message_group';
382
-
383
- if (context.threadIdFiltering) {
384
- if (context.listenOnlyThreads.length > 0 && !context.listenOnlyThreads.includes(message.threadId)) {
385
- context.logger.info(`[zalo listen] ${context.instanceKey} threadId: ${message.threadId} (not in listen-only list)`);
386
- return;
387
- }
388
- if (context.ignoreThreads.length > 0 && context.ignoreThreads.includes(message.threadId)) {
389
- context.logger.info(`[zalo listen] ${context.instanceKey} threadId : ${message.threadId} (in ignore list)`);
390
- return;
391
- }
392
- }
393
- if (context.keywordFiltering && (context.listenOnlyKeywords.length > 0 || context.ignoreKeywords.length > 0)) {
394
- let messageText = '';
395
- if (typeof message.data.content === 'string') {
396
- messageText = message.data.content;
397
- } else if (typeof message.data.content === 'object' && message.data.content !== null) {
398
- try {
399
- const parsedContent = JSON.parse(message.data.content);
400
- if (typeof parsedContent.title === 'string') {
401
- messageText = parsedContent.title;
402
- }
403
- } catch (e) {
404
- if (typeof message.data.content.title === 'string') { // Direct object check
405
- messageText = message.data.content.title;
406
- }
407
- }
408
- }
409
- const lowerCaseMessageText = messageText.toLowerCase();
410
- if (context.listenOnlyKeywords.length > 0 && !context.listenOnlyKeywords.some(keyword => lowerCaseMessageText.includes(keyword))) {
411
- context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lowerCaseMessageText} in ${message.threadId} ignored (no listen-only keyword found)`);
412
- return;
413
- }
414
- if (context.ignoreKeywords.length > 0 && context.ignoreKeywords.some(keyword => lowerCaseMessageText.includes(keyword))) {
415
- context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lowerCaseMessageText} in ${message.threadId} ignored (ignore keyword found)`);
416
- return;
417
- }
418
- }
419
- if (context.eventTypes.includes(eventType)) {
420
- await handleEvent(context, message, eventType);
421
- }
422
- });
423
- }
424
- if (context.eventTypes.includes('reaction')) {
425
- api.listener.on('reaction', async (reaction) => {
426
- if (context.ignoreSelfEvents && reaction.isSelf) {
427
- return;
428
- }
429
- await handleEvent(context, reaction, 'reaction');
430
- });
431
- }
432
- if (context.eventTypes.includes('undo')) {
433
- api.listener.on('undo', async (undo) => {
434
- if (context.ignoreSelfEvents && undo.isSelf) {
435
- return;
436
- }
437
- await handleEvent(context, undo, 'undo');
438
- });
439
- }
440
- if (context.eventTypes.includes('group_event')) {
441
- api.listener.on('group_event', async (groupEvent) => {
442
- if (context.ignoreSelfEvents && groupEvent.isSelf) {
443
- return;
444
- }
445
- await handleEvent(context, groupEvent, 'group_event');
446
- });
249
+ apiInstances.set(instanceKey, api);
250
+ }
251
+
252
+ // -------- listeners --------
253
+ const handleEvent = async (data, eventType) => {
254
+ if (!data?.isSelf) {
255
+ context.logger.info(`[${apiInstances.size}] [Zalo ${context.mode} received] ${context.credentialPhone}: ${eventType}`);
256
+ }
257
+
258
+ const dataWithContext = {
259
+ ...data,
260
+ _timestamp: new Date().toISOString(),
261
+ _instanceKey: context.instanceKey,
262
+ _name: context.credentialName,
263
+ _phone: context.credentialPhone,
264
+ _user_id: context.credentialUserId,
265
+ _eventType: eventType,
266
+ _source: 'zalo_trigger',
267
+ };
268
+
269
+ const webhookData = this.getWorkflowStaticData('node');
270
+ if (!webhookData[context.instanceKey]) {
271
+ webhookData[context.instanceKey] = {};
272
+ }
273
+ const instanceData = webhookData[context.instanceKey];
274
+ instanceData.lastMessage = data;
275
+ instanceData.lastMessageTime = new Date().toISOString();
276
+ instanceData.isConnected = true;
277
+ instanceData.eventTypes = context.eventTypes;
278
+ if (!instanceData.createdAt) {
279
+ instanceData.createdAt = new Date().toISOString();
280
+ }
281
+
282
+ emitEvent(dataWithContext);
283
+ };
284
+
285
+ if (context.eventTypes.includes('message_user') || context.eventTypes.includes('message_group')) {
286
+ api.listener.on('message', async (message) => {
287
+ const eventType = message.type === threadTypeUser ? 'message_user' : 'message_group';
288
+
289
+ if (context.threadIdFiltering) {
290
+ if (context.listenOnlyThreads.length > 0 && !context.listenOnlyThreads.includes(message.threadId)) {
291
+ context.logger.info(`[zalo listen] ${context.instanceKey} threadId: ${message.threadId} (not in listen-only list)`);
292
+ return;
447
293
  }
448
- if (context.eventTypes.includes('friend_request')) {
449
- api.listener.on('friend_event', async (friendEvent) => {
450
- if (context.ignoreSelfEvents && friendEvent.isSelf) {
451
- return;
452
- }
453
- await handleEvent(context, friendEvent, 'friend_request');
454
- });
294
+ if (context.ignoreThreads.length > 0 && context.ignoreThreads.includes(message.threadId)) {
295
+ context.logger.info(`[zalo listen] ${context.instanceKey} threadId : ${message.threadId} (in ignore list)`);
296
+ return;
455
297
  }
456
- async function handleEvent(context, data, eventType) {
457
- if (!data.isSelf) {
458
- context.logger.info(`[Zalo ${context.mode} received] ${context.credentialPhone}: ${eventType}`);
459
- }
298
+ }
460
299
 
461
- const dataWithContext = {
462
- ...data,
463
- _timestamp: new Date().toISOString(),
464
- _instanceKey: context.instanceKey,
465
- _name: context.credentialName,
466
- _phone: context.credentialPhone,
467
- _user_id: context.credentialUserId,
468
- _eventType: eventType,
469
- _source: 'zalo_trigger'
470
- };
300
+ if (context.keywordFiltering && (context.listenOnlyKeywords.length > 0 || context.ignoreKeywords.length > 0)) {
301
+ let messageText = '';
302
+ const content = message?.data?.content;
303
+
304
+ if (typeof content === 'string') {
305
+ messageText = content;
306
+ }
307
+ else if (typeof content === 'object' && content !== null) {
308
+ // parse JSON string; object.title
471
309
  try {
472
- await context.helpers.httpRequest({
473
- method: 'POST',
474
- url: context.webhookUrl,
475
- body: {
476
- message: dataWithContext,
477
- instanceKey: context.instanceKey,
478
- trigger: 'zalo_trigger'
479
- },
480
- headers: {
481
- 'Content-Type': 'application/json',
482
- },
483
- });
484
- }
485
- catch (webhookError) {
486
- const queue = messageQueue.get(context.instanceKey) || [];
487
- queue.push(dataWithContext);
488
- messageQueue.set(context.instanceKey, queue);
489
- context.logger.error(`Failed to trigger webhook for ${context.instanceKey}: ${webhookError.message} >> Event stored in fallback queue for ${context.instanceKey}, queue size: ${queue.length}`);
310
+ const parsedContent = typeof content === 'string' ? JSON.parse(content) : content;
311
+ if (typeof parsedContent?.title === 'string') {
312
+ messageText = parsedContent.title;
313
+ }
490
314
  }
491
- const webhookData = context.getWorkflowStaticData('node');
492
- if (!webhookData[context.instanceKey]) {
493
- webhookData[context.instanceKey] = {};
315
+ catch (e) {
316
+ if (typeof content?.title === 'string') {
317
+ messageText = content.title;
318
+ }
494
319
  }
495
- const instanceData = webhookData[context.instanceKey];
496
- instanceData.lastMessage = data;
497
- instanceData.lastMessageTime = new Date().toISOString();
498
320
  }
499
321
 
500
- api.listener.start();
501
- this.logger.info(`[Zalo ${mode}] [${apiInstances.size}] Started Listening: ${currentPhone} - ${currentName} (${context.eventTypes})`);
502
- const webhookData = this.getWorkflowStaticData('node');
503
- if (!webhookData[instanceKey]) {
504
- webhookData[instanceKey] = {};
322
+ const lower = (messageText || '').toLowerCase();
323
+ if (context.listenOnlyKeywords.length > 0 && !context.listenOnlyKeywords.some(k => lower.includes(k))) {
324
+ context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lower} in ${message.threadId} ignored (no listen-only keyword found)`);
325
+ return;
505
326
  }
506
- const instanceData = webhookData[instanceKey];
507
- instanceData.isConnected = true;
508
- instanceData.eventTypes = this.getNodeParameter('eventTypes', 0);
509
- instanceData.createdAt = new Date().toISOString();
510
- return true;
511
- }
512
- catch (error) {
513
- const continueOnFail = this.getNodeParameter('continueOnFail', 0, false);
514
- if (continueOnFail) {
515
- this.logger.error(`Zalo connection failed: ${currentPhone}. Skipping trigger. Error: ${error.message}`);
516
- const tokenOverride = this.getNodeParameter('telegramTokenOverride', 0, '');
517
- const chatIdOverride = this.getNodeParameter('telegramChatIdOverride', 0, '');
518
- const telegramToken = tokenOverride || credentials.telegramToken;
519
- const telegramChatId = chatIdOverride || credentials.telegramChatId;
520
-
521
- if (telegramToken && telegramChatId) {
522
- const errorMessage = `⚠️ **Lỗi Kích Hoạt Zalo Trigger**\n\n- Tài khoản: ${currentName}\n- SĐT: ${currentPhone}\n- Lý do: ${error.message} (bỏ qua node này tiếp tục active workflow)`;
523
- (0, zalo_helper_1.sendToTelegram)({
524
- token: telegramToken,
525
- chatId: telegramChatId,
526
- text: errorMessage,
527
- }).catch(e => this.logger.error(`Failed to send Telegram notification: ${e.message}`));
528
- }
529
- return false; // allow workflow to continue
327
+ if (context.ignoreKeywords.length > 0 && context.ignoreKeywords.some(k => lower.includes(k))) {
328
+ context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lower} in ${message.threadId} ignored (ignore keyword found)`);
329
+ return;
530
330
  }
531
- // stop workflow active
532
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo connection failed: ${error.message}`);
533
331
  }
534
- },
535
- async delete() {
536
- // this.getMode() === 'internal'
537
- const credentials = await this.getCredentials('zaloApi');
538
- const useSession = this.getNodeParameter('useSession', 0, false);
539
- const keyId = useSession ? this.getNodeParameter('connectToId', 0, '') : (credentials ? credentials.userId : '');
540
- const webhookUrl = this.getNodeWebhookUrl('default');
541
- const instanceKey = webhookUrl?.includes('/webhook-test/')
542
- ? `${keyId}-test`
543
- : `${keyId}`;
544
-
545
- if (apiInstances.has(instanceKey)) {
546
- const api = apiInstances.get(instanceKey);
547
- if (api) {
548
- api.listener.stop();
549
- apiInstances.delete(instanceKey);
550
- this.logger.info(`[Zalo Trigger Deactivated] Stopped and deleted instance: ${instanceKey}`);
551
- }
552
- }
553
- if (reconnectTimers.has(instanceKey)) {
554
- clearTimeout(reconnectTimers.get(instanceKey));
555
- reconnectTimers.delete(instanceKey);
332
+
333
+ if (context.eventTypes.includes(eventType)) {
334
+ await handleEvent(message, eventType);
556
335
  }
557
- if (messageQueue.has(instanceKey)) {
558
- messageQueue.delete(instanceKey);
336
+ });
337
+ }
338
+
339
+ if (context.eventTypes.includes('reaction')) {
340
+ api.listener.on('reaction', async (reaction) => {
341
+ if (context.ignoreSelfEvents && reaction.isSelf) return;
342
+ await handleEvent(reaction, 'reaction');
343
+ });
344
+ }
345
+
346
+ if (context.eventTypes.includes('undo')) {
347
+ api.listener.on('undo', async (undo) => {
348
+ if (context.ignoreSelfEvents && undo.isSelf) return;
349
+ await handleEvent(undo, 'undo');
350
+ });
351
+ }
352
+
353
+ if (context.eventTypes.includes('group_event')) {
354
+ api.listener.on('group_event', async (groupEvent) => {
355
+ if (context.ignoreSelfEvents && groupEvent.isSelf) return;
356
+ await handleEvent(groupEvent, 'group_event');
357
+ });
358
+ }
359
+
360
+ if (context.eventTypes.includes('friend_request')) {
361
+ api.listener.on('friend_event', async (friendEvent) => {
362
+ if (context.ignoreSelfEvents && friendEvent.isSelf) return;
363
+ await handleEvent(friendEvent, 'friend_request');
364
+ });
365
+ }
366
+
367
+ // Start listening
368
+ api.listener.start();
369
+ this.logger.info(`[${apiInstances.size}] [Zalo ${mode}] Listening: ${currentPhone} - ${currentName} (${context.eventTypes})`);
370
+
371
+ // manual within 60s
372
+ const manualTriggerFunction = async () => {
373
+ if (mode !== 'manual') return true;
374
+ return await new Promise(async (resolve, reject) => {
375
+ const timeout = setTimeout(() => {
376
+ closeFunction().then(() => {
377
+ reject(new n8n_workflow_1.NodeOperationError(this.getNode(), 'This 60 sec timeout is only set for "manually triggered execution"', {
378
+ description: 'Thời gian Excute Test là 60 giây, vui lòng nhắn gì đó để test',
379
+ }));
380
+ }).catch(cleanupError => {
381
+ this.logger.error(`[Zalo Trigger] Error during cleanup after manual test timeout for ${instanceKey}: ${cleanupError.message}`);
382
+ reject(new n8n_workflow_1.NodeOperationError(this.getNode(), `This 60 sec timeout is only set for "manually triggered execution" (err: ${cleanupError.message})`, {
383
+ description: 'Thời gian Excute Test là 60 giây, vui lòng nhắn gì đó để test',
384
+ }));
385
+ });
386
+ }, 60000);
387
+
388
+ const once = async (event, eventType) => {
389
+ try {
390
+ await handleEvent(event, eventType);
391
+ }
392
+ finally {
393
+ clearTimeout(timeout);
394
+ resolve(true);
395
+ }
396
+ };
397
+
398
+ const onMsg = (m) => {
399
+ const et = m.type === threadTypeUser ? 'message_user' : 'message_group';
400
+ once(m, et);
401
+ };
402
+ const onReaction = (e) => once(e, 'reaction');
403
+ const onUndo = (e) => once(e, 'undo');
404
+ const onGroup = (e) => once(e, 'group_event');
405
+ const onFriend = (e) => once(e, 'friend_request');
406
+
407
+ if (typeof api.listener.once === 'function') {
408
+ api.listener.once('message', onMsg);
409
+ api.listener.once('reaction', onReaction);
410
+ api.listener.once('undo', onUndo);
411
+ api.listener.once('group_event', onGroup);
412
+ api.listener.once('friend_event', onFriend);
559
413
  }
560
- const webhookData = this.getWorkflowStaticData('node');
561
- if (webhookData[instanceKey]) {
562
- delete webhookData[instanceKey];
414
+ else {
415
+ const cleanup = () => {
416
+ try { api.listener.off?.('message', onMsg); } catch { }
417
+ try { api.listener.off?.('reaction', onReaction); } catch { }
418
+ try { api.listener.off?.('undo', onUndo); } catch { }
419
+ try { api.listener.off?.('group_event', onGroup); } catch { }
420
+ try { api.listener.off?.('friend_event', onFriend); } catch { }
421
+ };
422
+ const wrapOnce = (fn) => async (...args) => { cleanup(); await fn(...args); };
423
+ api.listener.on('message', wrapOnce(onMsg));
424
+ api.listener.on('reaction', wrapOnce(onReaction));
425
+ api.listener.on('undo', wrapOnce(onUndo));
426
+ api.listener.on('group_event', wrapOnce(onGroup));
427
+ api.listener.on('friend_event', wrapOnce(onFriend));
563
428
  }
564
- return true;
565
- },
566
- },
567
- };
568
- }
569
- async webhook() {
570
- const req = this.getRequestObject();
571
- const body = req.body;
572
- const webhookData = this.getWorkflowStaticData('node');
573
- const allMessages = [];
574
- if (body && body.message && body.trigger === 'zalo_trigger') {
575
- allMessages.push(body.message);
576
- }
577
- else {
578
- for (const [instanceKey, messages] of messageQueue.entries()) {
579
- this.logger.info(`Processing fallback queued messages for instance: ${instanceKey}, message count: ${messages.length}`);
580
- if (messages.length > 0) {
581
- for (const message of messages) {
582
- allMessages.push(message);
583
- this.logger.info(`Added fallback queued message from ${instanceKey}`);
429
+ });
430
+ };
431
+
432
+ closeFunction = async () => {
433
+ const existing = apiInstances.get(instanceKey);
434
+ if (existing) {
435
+ try {
436
+ existing.listener.stop();
584
437
  }
585
- messageQueue.set(instanceKey, []);
586
- this.logger.info(`Cleared fallback message queue for ${instanceKey}`);
438
+ catch { }
439
+ apiInstances.delete(instanceKey);
440
+ this.logger.info(`[Zalo Trigger Deactivated] Stopped and deleted instance: ${instanceKey}`);
441
+ }
442
+ if (reconnectTimers.has(instanceKey)) {
443
+ clearTimeout(reconnectTimers.get(instanceKey));
444
+ reconnectTimers.delete(instanceKey);
445
+ }
446
+ const webhookData = this.getWorkflowStaticData('node');
447
+ if (webhookData[instanceKey]) {
448
+ delete webhookData[instanceKey];
587
449
  }
450
+
451
+ const workflowId = this.getWorkflow().id;
452
+ if (activationCleanupMap.has(workflowId)) {
453
+ activationCleanupMap.delete(workflowId);
454
+ this.logger.info(`[Zalo Trigger Deactivated] Cleared activation cleanup map for workflow: ${workflowId}`);
455
+ }
456
+ };
457
+
458
+ const workflowId = this.getWorkflow().id;
459
+ if (!activationCleanupMap.has(workflowId)) {
460
+ activationCleanupMap.set(workflowId, new Map());
588
461
  }
462
+ activationCleanupMap.get(workflowId).set(instanceKey, closeFunction);
463
+
464
+ return { closeFunction, manualTriggerFunction };
589
465
  }
590
- if (allMessages.length === 0) {
591
- this.logger.info('No messages found, returning empty trigger data');
592
- allMessages.push({
593
- _trigger: 'empty',
594
- _timestamp: new Date().toISOString(),
595
- _source: 'zalo_trigger',
596
- _debug: {
597
- webhookDataKeys: Object.keys(webhookData),
598
- messageQueueSize: messageQueue.size,
599
- messageQueueKeys: Array.from(messageQueue.keys()),
600
- requestBody: body
466
+ catch (error) {
467
+ const continueOnFail = this.getNodeParameter('continueOnFail', 0, false);
468
+ if (continueOnFail && mode !== 'manual') {
469
+ this.logger.error(`Zalo connection failed: ${currentPhone}: Skipping trigger. Error: ${error.message}`);
470
+ const tokenOverride = this.getNodeParameter('telegramTokenOverride', 0, '');
471
+ const chatIdOverride = this.getNodeParameter('telegramChatIdOverride', 0, '');
472
+ const telegramToken = tokenOverride || credentials.telegramToken;
473
+ const telegramChatId = chatIdOverride || credentials.telegramChatId;
474
+
475
+ if (telegramToken && telegramChatId) {
476
+ const errorMessage = `⚠️ **Lỗi Kích Hoạt Zalo Trigger**\n\n- Tài khoản: ${currentName}\n- SĐT: ${currentPhone}\n- Lý do: ${error.message} (bỏ qua node này tiếp tục active workflow)`;
477
+ (0, zalo_helper_1.sendToTelegram)({
478
+ token: telegramToken,
479
+ chatId: telegramChatId,
480
+ text: errorMessage,
481
+ }).catch(e => this.logger.error(`Failed to send Telegram notification: ${e.message}`));
482
+ }
483
+
484
+ return {
485
+ closeFunction: async () => { },
486
+ manualTriggerFunction: async () => true,
487
+ };
488
+ }
489
+
490
+ const workflowId = this.getWorkflow().id;
491
+ if (activationCleanupMap.has(workflowId)) {
492
+ this.logger.warn(`[Zalo Trigger] Activation failed. Cleaning up other triggers for workflow ${workflowId}.`);
493
+ for (const [key, cleanup] of activationCleanupMap.get(workflowId).entries()) {
494
+ await cleanup().catch(e => this.logger.error(`Error during cleanup of instance ${key}: ${e.message}`));
601
495
  }
602
- });
496
+ activationCleanupMap.delete(workflowId);
497
+ }
498
+
499
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo connection failed: ${currentPhone}: ${error.message}`);
603
500
  }
604
- return {
605
- workflowData: [this.helpers.returnJsonArray(allMessages)],
606
- };
607
501
  }
608
502
  }
609
503
  exports.ZaloTrigger = ZaloTrigger;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-zalo-custom",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "n8n nodes for Zalo automation. Send messages, manage groups, friends, and listen to events without third-party services.",
5
5
  "keywords": [
6
6
  "n8n",