n8n-nodes-zalo-custom 1.0.5 → 1.0.7
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 +9 -8
- package/nodes/ZaloTrigger/ZaloTrigger.node.js +348 -454
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# n8n-nodes-zalo-custom
|
|
2
2
|
|
|
3
|
-
Thư viện n8n nodes
|
|
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
|
|
|
@@ -10,6 +10,7 @@ Hoạt động độc lập trong môi trường **n8n của bạn** — **khôn
|
|
|
10
10
|
|
|
11
11
|
**Github:** [codedao12](https://github.com/codedao12)
|
|
12
12
|
**Email:** codedao12@gmail.com
|
|
13
|
+
**Video hướng dẫn:** [youtube](https://youtu.be/DPcGiIKUm1Q)
|
|
13
14
|
**Nhóm Zalo hỗ trợ:** [Tham gia nhóm Zalo](https://zalo.me/g/uinmin927)
|
|
14
15
|
Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
|
|
15
16
|
|
|
@@ -18,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.
|
|
|
18
19
|
|
|
19
20
|
| 💯 | Tính Năng |
|
|
20
21
|
|:----:|:--------------------------------------------------|
|
|
21
|
-
| 🔑 | _X
|
|
22
|
+
| 🔑 | _XÁC THỰC & KẾT NỐI_ |
|
|
22
23
|
| ✓ | Đăng nhập bằng Mã QR |
|
|
23
24
|
| ✓✓ | Expression Zalo Credential |
|
|
24
25
|
| ✓ | Hỗ trợ đăng nhập nhiều tài khoản, proxy |
|
|
25
26
|
| ✓✓ | Đăng nhập, thông báo qua telegram |
|
|
26
|
-
| ⚡️ |
|
|
27
|
+
| ⚡️ | _TRIGGER_ |
|
|
27
28
|
| ✓ | Sự kiện tin nhắn mới (lọc theo từ khóa, nhóm) |
|
|
28
29
|
| ✓ | Sự kiện thu hồi tin nhắn, thả cảm xúc |
|
|
29
30
|
| ✓ | Sự kiện trong nhóm (tham gia, rời, đổi quyền,...) |
|
|
30
31
|
| ✓ | Sự kiện về bạn bè (kết bạn, hủy bạn) |
|
|
31
|
-
| 💬 |
|
|
32
|
+
| 💬 | _NHẮN TIN_ |
|
|
32
33
|
| ✓ | Gửi tin nhắn (Văn bản, Ảnh, Sticker) |
|
|
33
34
|
| ✓ | Trả lời tin nhắn (Quote) |
|
|
34
35
|
| ✓ | Gắn thẻ (Tag) thành viên trong nhóm |
|
|
35
|
-
|
|
|
36
|
+
| ✓✓ | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
|
|
36
37
|
| ✓ | Thu hồi tin nhắn đã gửi |
|
|
37
38
|
| ✓ | Mô phỏng trạng thái "Đang soạn tin..." |
|
|
38
|
-
| 👤 |
|
|
39
|
+
| 👤 | _QUẢN LÝ TÀI KHOẢN & BẠN BÈ_ |
|
|
39
40
|
| ✓ | Gửi / Hủy lời mời kết bạn |
|
|
40
41
|
| ✓ | Chấp nhận / Từ chối lời mời kết bạn |
|
|
41
42
|
| ✓ | Hủy kết bạn (xóa bạn) |
|
|
@@ -44,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.
|
|
|
44
45
|
| ✓ | Lấy thông tin chi tiết người dùng |
|
|
45
46
|
| ✓ | Cập nhật thông tin cá nhân (Tên, Giới tính,...) |
|
|
46
47
|
| ✓ | Chặn / Bỏ chặn người dùng |
|
|
47
|
-
| 👥 |
|
|
48
|
+
| 👥 | _QUẢN LÝ NHÓM_ |
|
|
48
49
|
| ✓ | Tạo nhóm mới |
|
|
49
50
|
| ✓ | Thêm / Xóa thành viên khỏi nhóm |
|
|
50
51
|
| ✓ | Giải tán nhóm |
|
|
@@ -55,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.
|
|
|
55
56
|
| ✓ | Lấy thông tin chi tiết và thành viên nhóm |
|
|
56
57
|
| ✓ | Tham gia nhóm bằng link / Rời nhóm |
|
|
57
58
|
| ✓ | Tạo ghi chú (Note) trong nhóm |
|
|
58
|
-
| 🎨 |
|
|
59
|
+
| 🎨 | _KHÁC_ |
|
|
59
60
|
| ✓ | Tạo bình chọn (Poll) trong nhóm |
|
|
60
61
|
| ✓ | Quản lý thẻ phân loại (Tag) |
|
|
61
62
|
| ✓ | Tìm kiếm sticker |
|
|
@@ -5,15 +5,17 @@ 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
|
|
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 = {
|
|
15
17
|
displayName: 'Zalo Trigger (codedao12)',
|
|
16
|
-
name: '
|
|
18
|
+
name: 'ZaloTrigger',
|
|
17
19
|
icon: 'file:../shared/zalo.svg',
|
|
18
20
|
group: ['trigger'],
|
|
19
21
|
version: 1,
|
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
this.logger.
|
|
236
|
+
catch (dbError) {
|
|
237
|
+
this.logger.error(`[Zalo ${mode}] Failed to update user: ${newUserId}: ${dbError.message}`);
|
|
343
238
|
}
|
|
344
239
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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.
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
457
|
-
if (!data.isSelf) {
|
|
458
|
-
context.logger.info(`[Zalo ${context.mode} received] ${context.credentialPhone}: ${eventType}`);
|
|
459
|
-
}
|
|
298
|
+
}
|
|
460
299
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
591
|
-
this.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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