n8n-nodes-zalo-custom 1.0.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/LICENSE.md +19 -0
- package/README.md +235 -0
- package/credentials/N8nZaloApi.credentials.js +33 -0
- package/credentials/ZaloApi.credentials.js +107 -0
- package/credentials/shared/n8n.png +0 -0
- package/credentials/shared/zalo.svg +10 -0
- package/index.js +0 -0
- package/nodes/ZaloCommunication/ZaloCommunication.node.js +420 -0
- package/nodes/ZaloCommunication/ZaloCommunicationDescription.js +524 -0
- package/nodes/ZaloGroup/ZaloGroup.node.js +309 -0
- package/nodes/ZaloGroup/ZaloGroupDescription.js +318 -0
- package/nodes/ZaloLoginByQr/ZaloLoginByQr.node.js +579 -0
- package/nodes/ZaloSendMessage/ZaloSendMessage.node.js +773 -0
- package/nodes/ZaloTrigger/ZaloTrigger.node.js +609 -0
- package/nodes/ZaloUploadAttachment/ZaloUploadAttachment.node.js +265 -0
- package/nodes/ZaloUploadAttachment/ZaloUploadAttachmentDescription.js +187 -0
- package/nodes/ZaloUser/ZaloUser.node.js +212 -0
- package/nodes/ZaloUser/ZaloUserDescription.js +361 -0
- package/nodes/shared/ZaloNodeProperties.js +55 -0
- package/nodes/shared/zalo.svg +10 -0
- package/nodes/utils/crypto.helper.js +57 -0
- package/nodes/utils/helper.js +41 -0
- package/nodes/utils/zalo.helper.js +324 -0
- package/package.json +60 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZaloTrigger = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const ZaloNodeProperties_1 = require("../shared/ZaloNodeProperties");
|
|
6
|
+
const zca_js_1 = require("zca-js");
|
|
7
|
+
const zalo_helper_1 = require("../utils/zalo.helper");
|
|
8
|
+
const apiInstances = new Map();
|
|
9
|
+
const reconnectTimers = new Map();
|
|
10
|
+
const messageQueue = new Map();
|
|
11
|
+
const threadTypeUser = zca_js_1.ThreadType.User;
|
|
12
|
+
class ZaloTrigger {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.description = {
|
|
15
|
+
displayName: 'Zalo Trigger',
|
|
16
|
+
name: 'zaloApiTrigger',
|
|
17
|
+
icon: 'file:../shared/zalo.svg',
|
|
18
|
+
group: ['trigger'],
|
|
19
|
+
version: 1,
|
|
20
|
+
description: 'Sự kiện lắng nghe tin nhắn trên Zalo (multiple users)',
|
|
21
|
+
defaults: {
|
|
22
|
+
name: 'Zalo Trigger',
|
|
23
|
+
},
|
|
24
|
+
inputs: [],
|
|
25
|
+
outputs: ['main'],
|
|
26
|
+
webhooks: [
|
|
27
|
+
{
|
|
28
|
+
name: 'default',
|
|
29
|
+
httpMethod: 'POST',
|
|
30
|
+
responseMode: 'onReceived',
|
|
31
|
+
path: 'webhook',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
credentials: [...ZaloNodeProperties_1.zaloApiCredential],
|
|
35
|
+
properties: [
|
|
36
|
+
...ZaloNodeProperties_1.zaloSessionProperties,
|
|
37
|
+
{
|
|
38
|
+
displayName: 'Event Types',
|
|
39
|
+
name: 'eventTypes',
|
|
40
|
+
type: 'multiOptions',
|
|
41
|
+
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
|
+
},
|
|
72
|
+
],
|
|
73
|
+
default: ['message_user', 'message_group'],
|
|
74
|
+
required: true,
|
|
75
|
+
description: 'Các loại sự kiện muốn lắng nghe',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
displayName: 'Filtering Thread IDs',
|
|
79
|
+
name: 'threadIdFiltering',
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false,
|
|
82
|
+
displayOptions: {
|
|
83
|
+
show: {
|
|
84
|
+
eventTypes: [
|
|
85
|
+
'message_user',
|
|
86
|
+
'message_group',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
description: 'Chỉ định ID nhóm nhận sự kiện User/Group Message',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
displayName: 'Allowed Thread IDs',
|
|
94
|
+
name: 'listenOnlyThreads',
|
|
95
|
+
type: 'string',
|
|
96
|
+
typeOptions: {
|
|
97
|
+
textArea: true,
|
|
98
|
+
},
|
|
99
|
+
default: '',
|
|
100
|
+
displayOptions: {
|
|
101
|
+
show: {
|
|
102
|
+
threadIdFiltering: [
|
|
103
|
+
true,
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
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
|
+
placeholder: 'threadId1,threadId2,threadId3',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
displayName: 'Blocked Thread IDs',
|
|
112
|
+
name: 'ignoreThreads',
|
|
113
|
+
type: 'string',
|
|
114
|
+
typeOptions: {
|
|
115
|
+
textArea: true,
|
|
116
|
+
},
|
|
117
|
+
default: '',
|
|
118
|
+
displayOptions: {
|
|
119
|
+
show: {
|
|
120
|
+
threadIdFiltering: [
|
|
121
|
+
true,
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
description: 'Bỏ qua tin nhắn từ các thread ID này (phân cách bằng dấu phẩy).',
|
|
126
|
+
placeholder: 'threadId4,threadId5',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
displayName: 'Filtering Keyword',
|
|
130
|
+
name: 'keywordFiltering',
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
default: false,
|
|
133
|
+
displayOptions: {
|
|
134
|
+
show: {
|
|
135
|
+
eventTypes: [
|
|
136
|
+
'message_user',
|
|
137
|
+
'message_group',
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
description: 'Lọc tin nhắn theo từ khoá khi nhận sự kiện User/Group Message',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
displayName: 'Listen Only Keywords',
|
|
145
|
+
name: 'listenOnlyKeywords',
|
|
146
|
+
type: 'string',
|
|
147
|
+
typeOptions: {
|
|
148
|
+
textArea: true,
|
|
149
|
+
},
|
|
150
|
+
default: '',
|
|
151
|
+
displayOptions: {
|
|
152
|
+
show: {
|
|
153
|
+
keywordFiltering: [
|
|
154
|
+
true,
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
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
|
+
placeholder: 'ok, chào, tin tức',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
displayName: 'Ignore Keywords',
|
|
163
|
+
name: 'ignoreKeywords',
|
|
164
|
+
type: 'string',
|
|
165
|
+
typeOptions: {
|
|
166
|
+
textArea: true,
|
|
167
|
+
},
|
|
168
|
+
default: '',
|
|
169
|
+
displayOptions: {
|
|
170
|
+
show: {
|
|
171
|
+
keywordFiltering: [
|
|
172
|
+
true,
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
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
|
+
placeholder: 'spam, quảng cáo',
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
displayName: 'Self Listen',
|
|
182
|
+
name: 'selfListen',
|
|
183
|
+
type: 'boolean',
|
|
184
|
+
default: false,
|
|
185
|
+
required: true,
|
|
186
|
+
displayOptions: {
|
|
187
|
+
show: {
|
|
188
|
+
eventTypes: [
|
|
189
|
+
'message_user',
|
|
190
|
+
'message_group',
|
|
191
|
+
'reaction',
|
|
192
|
+
'undo',
|
|
193
|
+
'group_event',
|
|
194
|
+
'friend_request',
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
description: 'Lắng nghe các sự kiện do chính bạn thực hiện.',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
displayName: 'Self Messages Only',
|
|
202
|
+
name: 'ignoreSelfEvents',
|
|
203
|
+
type: 'boolean',
|
|
204
|
+
default: true,
|
|
205
|
+
displayOptions: {
|
|
206
|
+
show: {
|
|
207
|
+
selfListen: [
|
|
208
|
+
true,
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
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
|
+
},
|
|
214
|
+
{
|
|
215
|
+
displayName: 'Continue On Login Fail',
|
|
216
|
+
name: 'continueOnFail',
|
|
217
|
+
type: 'boolean',
|
|
218
|
+
default: false,
|
|
219
|
+
displayOptions: {
|
|
220
|
+
show: {
|
|
221
|
+
eventTypes: [
|
|
222
|
+
'message_user',
|
|
223
|
+
'message_group',
|
|
224
|
+
'reaction',
|
|
225
|
+
'undo',
|
|
226
|
+
'group_event',
|
|
227
|
+
'friend_request',
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
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
|
+
},
|
|
233
|
+
{
|
|
234
|
+
displayName: 'Telegram Bot Token',
|
|
235
|
+
name: 'telegramTokenOverride',
|
|
236
|
+
type: 'string',
|
|
237
|
+
typeOptions: {
|
|
238
|
+
password: false,
|
|
239
|
+
},
|
|
240
|
+
default: '',
|
|
241
|
+
displayOptions: {
|
|
242
|
+
show: {
|
|
243
|
+
continueOnFail: [true],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
description: 'Bot token dùng để thông báo khi trigger lỗi (để trống nếu credential đã có sẵn)',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
displayName: 'Telegram Chat ID',
|
|
250
|
+
name: 'telegramChatIdOverride',
|
|
251
|
+
type: 'string',
|
|
252
|
+
default: '',
|
|
253
|
+
displayOptions: {
|
|
254
|
+
show: {
|
|
255
|
+
continueOnFail: [true],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
description: 'ID nhóm chat dùng để thông báo khi trigger lỗi (để trống nếu credential đã có sẵn)',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
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
|
+
}
|
|
276
|
+
|
|
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
|
+
}
|
|
302
|
+
|
|
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
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
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
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.logger.warn(`[Zalo ${mode}] Could not fetch complete account info for ${instanceKey}.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
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
|
+
};
|
|
378
|
+
|
|
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
|
+
});
|
|
447
|
+
}
|
|
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
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async function handleEvent(context, data, eventType) {
|
|
457
|
+
if (!data.isSelf) {
|
|
458
|
+
context.logger.info(`[Zalo ${context.mode} received] ${context.credentialPhone}: ${eventType}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
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
|
+
};
|
|
471
|
+
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}`);
|
|
490
|
+
}
|
|
491
|
+
const webhookData = context.getWorkflowStaticData('node');
|
|
492
|
+
if (!webhookData[context.instanceKey]) {
|
|
493
|
+
webhookData[context.instanceKey] = {};
|
|
494
|
+
}
|
|
495
|
+
const instanceData = webhookData[context.instanceKey];
|
|
496
|
+
instanceData.lastMessage = data;
|
|
497
|
+
instanceData.lastMessageTime = new Date().toISOString();
|
|
498
|
+
}
|
|
499
|
+
|
|
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] = {};
|
|
505
|
+
}
|
|
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
|
|
530
|
+
}
|
|
531
|
+
// stop workflow active
|
|
532
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo connection failed: ${error.message}`);
|
|
533
|
+
}
|
|
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);
|
|
556
|
+
}
|
|
557
|
+
if (messageQueue.has(instanceKey)) {
|
|
558
|
+
messageQueue.delete(instanceKey);
|
|
559
|
+
}
|
|
560
|
+
const webhookData = this.getWorkflowStaticData('node');
|
|
561
|
+
if (webhookData[instanceKey]) {
|
|
562
|
+
delete webhookData[instanceKey];
|
|
563
|
+
}
|
|
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}`);
|
|
584
|
+
}
|
|
585
|
+
messageQueue.set(instanceKey, []);
|
|
586
|
+
this.logger.info(`Cleared fallback message queue for ${instanceKey}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
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
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
workflowData: [this.helpers.returnJsonArray(allMessages)],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
exports.ZaloTrigger = ZaloTrigger;
|