n8n-nodes-zalo-custom 1.0.7 → 1.0.9
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 +13 -9
- package/nodes/ZaloGroup/ZaloGroup.node.js +21 -0
- package/nodes/ZaloGroup/ZaloGroupDescription.js +69 -1
- package/nodes/ZaloTrigger/ZaloTrigger.node.js +51 -27
- package/nodes/ZaloUser/ZaloUser.node.js +75 -0
- package/nodes/ZaloUser/ZaloUserDescription.js +35 -2
- package/nodes/utils/zalo.helper.js +7 -55
- 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 tự động hoá Zalo thông qua n8n workflow. Cho phép
|
|
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,27 @@ 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_
|
|
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
|
-
|
|
|
32
|
-
|
|
|
31
|
+
| ✓✓ | Sự kiện về bạn bè (kết bạn, hủy bạn) |
|
|
32
|
+
| ✓✓ | Sự kiện bạn bè đã xem tin nhắn |
|
|
33
|
+
| ✓✓ | Sự kiện bạn bè đang soạn tin |
|
|
34
|
+
| 💬 | **_NHẮN TIN_** |
|
|
33
35
|
| ✓ | Gửi tin nhắn (Văn bản, Ảnh, Sticker) |
|
|
34
36
|
| ✓ | Trả lời tin nhắn (Quote) |
|
|
35
37
|
| ✓ | Gắn thẻ (Tag) thành viên trong nhóm |
|
|
36
38
|
| ✓✓ | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
|
|
37
39
|
| ✓ | Thu hồi tin nhắn đã gửi |
|
|
40
|
+
| ✓✓ | Lấy danh sách tin nhắn cũ |
|
|
38
41
|
| ✓ | Mô phỏng trạng thái "Đang soạn tin..." |
|
|
39
|
-
| 👤 | _QUẢN LÝ TÀI KHOẢN & BẠN BÈ_
|
|
42
|
+
| 👤 | **_QUẢN LÝ TÀI KHOẢN & BẠN BÈ_** |
|
|
40
43
|
| ✓ | Gửi / Hủy lời mời kết bạn |
|
|
41
44
|
| ✓ | Chấp nhận / Từ chối lời mời kết bạn |
|
|
42
45
|
| ✓ | Hủy kết bạn (xóa bạn) |
|
|
@@ -45,18 +48,19 @@ Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
|
|
|
45
48
|
| ✓ | Lấy thông tin chi tiết người dùng |
|
|
46
49
|
| ✓ | Cập nhật thông tin cá nhân (Tên, Giới tính,...) |
|
|
47
50
|
| ✓ | Chặn / Bỏ chặn người dùng |
|
|
48
|
-
| 👥 | _QUẢN LÝ NHÓM_
|
|
51
|
+
| 👥 | **_QUẢN LÝ NHÓM_** |
|
|
49
52
|
| ✓ | Tạo nhóm mới |
|
|
50
53
|
| ✓ | Thêm / Xóa thành viên khỏi nhóm |
|
|
54
|
+
| ✓✓ | Xóa tin nhắn thành viên |
|
|
51
55
|
| ✓ | Giải tán nhóm |
|
|
52
56
|
| ✓ | Thay đổi tên & ảnh đại diện nhóm |
|
|
53
57
|
| ✓ | Bổ nhiệm quyền Phó nhóm |
|
|
54
58
|
| ✓✓ | Chuyển quyền Trưởng nhóm |
|
|
55
59
|
| ✓ | Lấy danh sách tất cả các nhóm đã tham gia |
|
|
56
|
-
| ✓ | Lấy thông tin
|
|
60
|
+
| ✓ | Lấy thông tin nhóm (từ ID hoặc link) |
|
|
57
61
|
| ✓ | Tham gia nhóm bằng link / Rời nhóm |
|
|
58
62
|
| ✓ | Tạo ghi chú (Note) trong nhóm |
|
|
59
|
-
| 🎨 | _KHÁC_
|
|
63
|
+
| 🎨 | **_KHÁC_** |
|
|
60
64
|
| ✓ | Tạo bình chọn (Poll) trong nhóm |
|
|
61
65
|
| ✓ | Quản lý thẻ phân loại (Tag) |
|
|
62
66
|
| ✓ | Tìm kiếm sticker |
|
|
@@ -263,6 +263,12 @@ class ZaloGroup {
|
|
|
263
263
|
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
264
264
|
break;
|
|
265
265
|
}
|
|
266
|
+
case 'getGroupLinkInfo': {
|
|
267
|
+
const groupLink = this.getNodeParameter('groupLink', i);
|
|
268
|
+
const response = await api.getGroupLinkInfo({ link: groupLink });
|
|
269
|
+
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
266
272
|
case 'joinGroupLink': {
|
|
267
273
|
const groupLink = this.getNodeParameter('groupLink', i);
|
|
268
274
|
const response = await api.joinGroupLink(groupLink);
|
|
@@ -282,6 +288,21 @@ class ZaloGroup {
|
|
|
282
288
|
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
283
289
|
break;
|
|
284
290
|
}
|
|
291
|
+
case 'deleteMessage': {
|
|
292
|
+
const groupId = this.getNodeParameter('groupId', i);
|
|
293
|
+
const uidFrom = this.getNodeParameter('userId', i);
|
|
294
|
+
const msgId = this.getNodeParameter('msgId', i);
|
|
295
|
+
const cliMsgId = this.getNodeParameter('cliMsgId', i);
|
|
296
|
+
const onlyMe = this.getNodeParameter('onlyMe', i, true);
|
|
297
|
+
const dest = {
|
|
298
|
+
data: { cliMsgId, msgId, uidFrom },
|
|
299
|
+
threadId: groupId,
|
|
300
|
+
type: 1, // ThreadType.Group
|
|
301
|
+
};
|
|
302
|
+
const response = await api.deleteMessage(dest, onlyMe);
|
|
303
|
+
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
308
|
}
|
|
@@ -79,6 +79,12 @@ exports.zaloGroupOperations = [
|
|
|
79
79
|
description: 'Lấy Link Tham Gia Nhóm',
|
|
80
80
|
action: 'Lấy Link Mời Nhóm',
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Lấy Thông Tin Từ Link Nhóm',
|
|
84
|
+
value: 'getGroupLinkInfo',
|
|
85
|
+
description: 'Lấy thông tin của một nhóm từ link mời',
|
|
86
|
+
action: 'Lấy Thông Tin Từ Link Nhóm',
|
|
87
|
+
},
|
|
82
88
|
{
|
|
83
89
|
name: 'Xóa Thành Viên Khỏi Nhóm',
|
|
84
90
|
value: 'removeUserFromGroup',
|
|
@@ -103,6 +109,12 @@ exports.zaloGroupOperations = [
|
|
|
103
109
|
description: 'Giải tán một nhóm. Chỉ chủ nhóm mới có thể thực hiện.',
|
|
104
110
|
action: 'Giải Tán Nhóm',
|
|
105
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Xóa Tin Nhắn Của Thành Viên',
|
|
114
|
+
value: 'deleteMessage',
|
|
115
|
+
description: 'Xóa tin nhắn của thành viên trong nhóm (yêu cầu quyền admin)',
|
|
116
|
+
action: 'Xóa Tin Nhắn Của Thành Viên',
|
|
117
|
+
},
|
|
106
118
|
],
|
|
107
119
|
default: 'createGroup',
|
|
108
120
|
},
|
|
@@ -144,6 +156,7 @@ exports.zaloGroupFields = [
|
|
|
144
156
|
'changeGroupOwner',
|
|
145
157
|
'leaveGroup',
|
|
146
158
|
'disperseGroup',
|
|
159
|
+
'deleteMessage',
|
|
147
160
|
],
|
|
148
161
|
},
|
|
149
162
|
},
|
|
@@ -158,7 +171,7 @@ exports.zaloGroupFields = [
|
|
|
158
171
|
displayOptions: {
|
|
159
172
|
show: {
|
|
160
173
|
resource: ['zaloGroup'],
|
|
161
|
-
operation: ['addGroupDeputy', 'changeGroupOwner'],
|
|
174
|
+
operation: ['addGroupDeputy', 'changeGroupOwner', 'deleteMessage'],
|
|
162
175
|
},
|
|
163
176
|
},
|
|
164
177
|
description: 'ID của người dùng',
|
|
@@ -301,6 +314,47 @@ exports.zaloGroupFields = [
|
|
|
301
314
|
},
|
|
302
315
|
description: 'Link để tham gia nhóm (ví dụ: https://zalo.me/g/xxxxxx)',
|
|
303
316
|
},
|
|
317
|
+
{
|
|
318
|
+
displayName: 'Message ID',
|
|
319
|
+
name: 'msgId',
|
|
320
|
+
type: 'string',
|
|
321
|
+
required: true,
|
|
322
|
+
default: '',
|
|
323
|
+
displayOptions: {
|
|
324
|
+
show: {
|
|
325
|
+
resource: ['zaloGroup'],
|
|
326
|
+
operation: ['deleteMessage'],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
description: 'msgId của tin nhắn cần xóa',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
displayName: 'Client Message ID',
|
|
333
|
+
name: 'cliMsgId',
|
|
334
|
+
type: 'string',
|
|
335
|
+
required: true,
|
|
336
|
+
default: '',
|
|
337
|
+
displayOptions: {
|
|
338
|
+
show: {
|
|
339
|
+
resource: ['zaloGroup'],
|
|
340
|
+
operation: ['deleteMessage'],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
description: 'cliMsgId của tin nhắn cần xóa',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
displayName: 'Only Me',
|
|
347
|
+
name: 'onlyMe',
|
|
348
|
+
type: 'boolean',
|
|
349
|
+
default: true,
|
|
350
|
+
displayOptions: {
|
|
351
|
+
show: {
|
|
352
|
+
resource: ['zaloGroup'],
|
|
353
|
+
operation: ['deleteMessage'],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
description: 'Bật để xóa tin nhắn ở phía bạn. Tắt để xóa với mọi người (quyền admin)',
|
|
357
|
+
},
|
|
304
358
|
// {
|
|
305
359
|
// displayName: 'Giới Hạn',
|
|
306
360
|
// name: 'limit',
|
|
@@ -315,4 +369,18 @@ exports.zaloGroupFields = [
|
|
|
315
369
|
// },
|
|
316
370
|
// description: 'Số lượng tối đa cần lấy',
|
|
317
371
|
// },
|
|
372
|
+
{
|
|
373
|
+
displayName: 'Link Mời Nhóm',
|
|
374
|
+
name: 'groupLink',
|
|
375
|
+
type: 'string',
|
|
376
|
+
required: true,
|
|
377
|
+
default: '',
|
|
378
|
+
displayOptions: {
|
|
379
|
+
show: {
|
|
380
|
+
resource: ['zaloGroup'],
|
|
381
|
+
operation: ['getGroupLinkInfo'],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
description: 'Link để lấy thông tin nhóm (ví dụ: https://zalo.me/g/xxxxxx)',
|
|
385
|
+
},
|
|
318
386
|
];
|
|
@@ -15,7 +15,7 @@ class ZaloTrigger {
|
|
|
15
15
|
constructor() {
|
|
16
16
|
this.description = {
|
|
17
17
|
displayName: 'Zalo Trigger (codedao12)',
|
|
18
|
-
name: '
|
|
18
|
+
name: 'zaloApiTrigger',
|
|
19
19
|
icon: 'file:../shared/zalo.svg',
|
|
20
20
|
group: ['trigger'],
|
|
21
21
|
version: 1,
|
|
@@ -39,6 +39,8 @@ class ZaloTrigger {
|
|
|
39
39
|
{ name: 'Undo', value: 'undo', description: 'Sự kiện thu hồi tin nhắn' },
|
|
40
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
41
|
{ name: 'Friend Event', value: 'friend_request', description: 'Sự kiện có người gửi yêu cầu kết bạn' },
|
|
42
|
+
{ name: 'Seen Message', value: 'seen_message', description: 'Sự kiện đã xem tin nhắn (Chat riêng, Không Self Listen)' },
|
|
43
|
+
{ name: 'Typing', value: 'typing', description: 'Sự kiện đang gõ phím (Chat riêng, Không Self Listen)' },
|
|
42
44
|
],
|
|
43
45
|
default: ['message_user', 'message_group'],
|
|
44
46
|
required: true,
|
|
@@ -50,7 +52,7 @@ class ZaloTrigger {
|
|
|
50
52
|
type: 'boolean',
|
|
51
53
|
default: false,
|
|
52
54
|
displayOptions: { show: { eventTypes: ['message_user', 'message_group'] } },
|
|
53
|
-
description: 'Chỉ định ID nhóm nhận sự kiện User/Group Message',
|
|
55
|
+
description: 'Chỉ định ID nhóm nhận sự kiện User/Group Message (không khả dụng với Execute test)',
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
58
|
displayName: 'Allowed Thread IDs',
|
|
@@ -59,7 +61,7 @@ class ZaloTrigger {
|
|
|
59
61
|
typeOptions: { textArea: true },
|
|
60
62
|
default: '',
|
|
61
63
|
displayOptions: { show: { threadIdFiltering: [true] } },
|
|
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
|
|
64
|
+
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ả',
|
|
63
65
|
placeholder: 'threadId1,threadId2,threadId3',
|
|
64
66
|
},
|
|
65
67
|
{
|
|
@@ -69,7 +71,7 @@ class ZaloTrigger {
|
|
|
69
71
|
typeOptions: { textArea: true },
|
|
70
72
|
default: '',
|
|
71
73
|
displayOptions: { show: { threadIdFiltering: [true] } },
|
|
72
|
-
description: 'Bỏ qua tin nhắn từ các thread ID này (phân cách bằng dấu phẩy)
|
|
74
|
+
description: 'Bỏ qua tin nhắn từ các thread ID này (phân cách bằng dấu phẩy)',
|
|
73
75
|
placeholder: 'threadId4,threadId5',
|
|
74
76
|
},
|
|
75
77
|
{
|
|
@@ -78,7 +80,7 @@ class ZaloTrigger {
|
|
|
78
80
|
type: 'boolean',
|
|
79
81
|
default: false,
|
|
80
82
|
displayOptions: { show: { eventTypes: ['message_user', 'message_group'] } },
|
|
81
|
-
description: 'Lọc tin nhắn theo từ khoá khi nhận sự kiện User/Group Message',
|
|
83
|
+
description: 'Lọc tin nhắn theo từ khoá khi nhận sự kiện User/Group Message (không khả dụng với Execute test)',
|
|
82
84
|
},
|
|
83
85
|
{
|
|
84
86
|
displayName: 'Listen Only Keywords',
|
|
@@ -117,7 +119,7 @@ class ZaloTrigger {
|
|
|
117
119
|
type: 'boolean',
|
|
118
120
|
default: true,
|
|
119
121
|
displayOptions: { show: { selfListen: [true] } },
|
|
120
|
-
description: 'Self Listen chỉ nhận sự kiện User/Group Message (
|
|
122
|
+
description: 'Self Listen chỉ nhận sự kiện User/Group Message (không khả dụng với Execute test)',
|
|
121
123
|
},
|
|
122
124
|
{
|
|
123
125
|
displayName: 'Continue On Login Fail',
|
|
@@ -158,9 +160,9 @@ class ZaloTrigger {
|
|
|
158
160
|
let currentUserId = credentials ? credentials.userId : 'N/A';
|
|
159
161
|
let currentName = credentials ? credentials.nameAccount : 'N/A';
|
|
160
162
|
let currentPhone = credentials ? credentials.phoneNumber : 'N/A';
|
|
161
|
-
|
|
162
163
|
const mode = this.getMode(); // 'trigger' or 'manual'
|
|
163
164
|
const selfListen = this.getNodeParameter('selfListen', 0, false);
|
|
165
|
+
const continueOnFail = this.getNodeParameter('continueOnFail', 0, false);
|
|
164
166
|
|
|
165
167
|
const productionKey = `${keyId}`;
|
|
166
168
|
const manualKey = `${keyId}-manual`;
|
|
@@ -195,16 +197,30 @@ class ZaloTrigger {
|
|
|
195
197
|
.map(k => k.trim().toLowerCase()).filter(k => k && k.length > 1),
|
|
196
198
|
logger: this.logger,
|
|
197
199
|
mode,
|
|
200
|
+
continueOnFail: continueOnFail,
|
|
198
201
|
};
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
let telegramToken;
|
|
204
|
+
let telegramChatId;
|
|
205
|
+
if (continueOnFail) {
|
|
206
|
+
const tokenOverride = this.getNodeParameter('telegramTokenOverride', 0, '');
|
|
207
|
+
const chatIdOverride = this.getNodeParameter('telegramChatIdOverride', 0, '');
|
|
208
|
+
const credsToken = credentials ? credentials.telegramToken : '';
|
|
209
|
+
const credsChatId = credentials ? credentials.telegramChatId : '';
|
|
210
|
+
telegramToken = tokenOverride || credsToken;
|
|
211
|
+
telegramChatId = chatIdOverride || credsChatId;
|
|
212
|
+
if (!telegramToken || !telegramChatId) {
|
|
213
|
+
context.errorNotification = 'Vui lòng cung cấp BotToken và Chat_id để nhận thông báo khi phát hiện lỗi';
|
|
214
|
+
}
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
let api = apiInstances.get(instanceKey);
|
|
205
218
|
let closeFunction = async () => { };
|
|
206
219
|
|
|
207
220
|
try {
|
|
221
|
+
if (apiInstances.has(instanceKey)) {
|
|
222
|
+
this.logger.info(`Instance ${instanceKey} already exists, reusing...`);
|
|
223
|
+
}
|
|
208
224
|
if (!api) {
|
|
209
225
|
api = await (0, zalo_helper_1.getZaloApiClient)(this, { selfListen });
|
|
210
226
|
if (!api) {
|
|
@@ -261,7 +277,9 @@ class ZaloTrigger {
|
|
|
261
277
|
_instanceKey: context.instanceKey,
|
|
262
278
|
_name: context.credentialName,
|
|
263
279
|
_phone: context.credentialPhone,
|
|
264
|
-
|
|
280
|
+
_userId: context.credentialUserId,
|
|
281
|
+
_continueOnFail: context.continueOnFail,
|
|
282
|
+
_errorNotification: context.errorNotification,
|
|
265
283
|
_eventType: eventType,
|
|
266
284
|
_source: 'zalo_trigger',
|
|
267
285
|
};
|
|
@@ -285,7 +303,9 @@ class ZaloTrigger {
|
|
|
285
303
|
if (context.eventTypes.includes('message_user') || context.eventTypes.includes('message_group')) {
|
|
286
304
|
api.listener.on('message', async (message) => {
|
|
287
305
|
const eventType = message.type === threadTypeUser ? 'message_user' : 'message_group';
|
|
288
|
-
|
|
306
|
+
if (!context.eventTypes.includes(eventType)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
289
309
|
if (context.threadIdFiltering) {
|
|
290
310
|
if (context.listenOnlyThreads.length > 0 && !context.listenOnlyThreads.includes(message.threadId)) {
|
|
291
311
|
context.logger.info(`[zalo listen] ${context.instanceKey} threadId: ${message.threadId} (not in listen-only list)`);
|
|
@@ -296,11 +316,9 @@ class ZaloTrigger {
|
|
|
296
316
|
return;
|
|
297
317
|
}
|
|
298
318
|
}
|
|
299
|
-
|
|
300
319
|
if (context.keywordFiltering && (context.listenOnlyKeywords.length > 0 || context.ignoreKeywords.length > 0)) {
|
|
301
320
|
let messageText = '';
|
|
302
321
|
const content = message?.data?.content;
|
|
303
|
-
|
|
304
322
|
if (typeof content === 'string') {
|
|
305
323
|
messageText = content;
|
|
306
324
|
}
|
|
@@ -318,7 +336,6 @@ class ZaloTrigger {
|
|
|
318
336
|
}
|
|
319
337
|
}
|
|
320
338
|
}
|
|
321
|
-
|
|
322
339
|
const lower = (messageText || '').toLowerCase();
|
|
323
340
|
if (context.listenOnlyKeywords.length > 0 && !context.listenOnlyKeywords.some(k => lower.includes(k))) {
|
|
324
341
|
context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lower} in ${message.threadId} ignored (no listen-only keyword found)`);
|
|
@@ -329,10 +346,7 @@ class ZaloTrigger {
|
|
|
329
346
|
return;
|
|
330
347
|
}
|
|
331
348
|
}
|
|
332
|
-
|
|
333
|
-
if (context.eventTypes.includes(eventType)) {
|
|
334
|
-
await handleEvent(message, eventType);
|
|
335
|
-
}
|
|
349
|
+
await handleEvent(message, eventType);
|
|
336
350
|
});
|
|
337
351
|
}
|
|
338
352
|
|
|
@@ -364,11 +378,28 @@ class ZaloTrigger {
|
|
|
364
378
|
});
|
|
365
379
|
}
|
|
366
380
|
|
|
381
|
+
if (context.eventTypes.includes('seen_message')) {
|
|
382
|
+
api.listener.on('seen_messages', async (seenMessages) => {
|
|
383
|
+
for (const seen of seenMessages) {
|
|
384
|
+
if (seen.type === threadTypeUser && !seen.isSelf) {
|
|
385
|
+
await handleEvent(seen, 'seen_message');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (context.eventTypes.includes('typing')) {
|
|
392
|
+
api.listener.on('typing', async (typing) => {
|
|
393
|
+
if (seen.type === threadTypeUser && !seen.isSelf) {
|
|
394
|
+
await handleEvent(typing, 'typing');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
367
399
|
// Start listening
|
|
368
|
-
api.listener.start();
|
|
400
|
+
api.listener.start({ retryOnClose: true });
|
|
369
401
|
this.logger.info(`[${apiInstances.size}] [Zalo ${mode}] Listening: ${currentPhone} - ${currentName} (${context.eventTypes})`);
|
|
370
402
|
|
|
371
|
-
// manual within 60s
|
|
372
403
|
const manualTriggerFunction = async () => {
|
|
373
404
|
if (mode !== 'manual') return true;
|
|
374
405
|
return await new Promise(async (resolve, reject) => {
|
|
@@ -466,12 +497,6 @@ class ZaloTrigger {
|
|
|
466
497
|
catch (error) {
|
|
467
498
|
const continueOnFail = this.getNodeParameter('continueOnFail', 0, false);
|
|
468
499
|
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
500
|
if (telegramToken && telegramChatId) {
|
|
476
501
|
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
502
|
(0, zalo_helper_1.sendToTelegram)({
|
|
@@ -480,7 +505,6 @@ class ZaloTrigger {
|
|
|
480
505
|
text: errorMessage,
|
|
481
506
|
}).catch(e => this.logger.error(`Failed to send Telegram notification: ${e.message}`));
|
|
482
507
|
}
|
|
483
|
-
|
|
484
508
|
return {
|
|
485
509
|
closeFunction: async () => { },
|
|
486
510
|
manualTriggerFunction: async () => true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZaloUser = void 0;
|
|
4
|
+
const zca_js_1 = require("zca-js");
|
|
4
5
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
6
|
const ZaloNodeProperties_1 = require("../shared/ZaloNodeProperties");
|
|
6
7
|
const ZaloUserDescription_1 = require("./ZaloUserDescription");
|
|
@@ -118,6 +119,17 @@ class ZaloUser {
|
|
|
118
119
|
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
119
120
|
break;
|
|
120
121
|
}
|
|
122
|
+
case 'getFriendOnlines': {
|
|
123
|
+
const response = await api.getFriendOnlines();
|
|
124
|
+
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'getLastOnline': {
|
|
128
|
+
const userId = this.getNodeParameter('userId', i);
|
|
129
|
+
const response = await api.lastOnline(userId);
|
|
130
|
+
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
121
133
|
case 'findUser': {
|
|
122
134
|
const phoneNumber = this.getNodeParameter('phoneNumber', i);
|
|
123
135
|
const response = await api.findUser(phoneNumber);
|
|
@@ -185,6 +197,69 @@ class ZaloUser {
|
|
|
185
197
|
returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
|
|
186
198
|
break;
|
|
187
199
|
}
|
|
200
|
+
case 'getOldMessages': {
|
|
201
|
+
const type = this.getNodeParameter('threadType', i);
|
|
202
|
+
const lastMsgId = this.getNodeParameter('lastMsgId', i, null);
|
|
203
|
+
api.listener.start({ retryOnClose: false });
|
|
204
|
+
const getMessages = () =>
|
|
205
|
+
new Promise((resolve, reject) => {
|
|
206
|
+
const timeout = setTimeout(() => {
|
|
207
|
+
api.listener.off('old_messages', onOldMessages);
|
|
208
|
+
reject(new Error('Timeout: Did not receive old messages within 30 seconds.'));
|
|
209
|
+
}, 30000);
|
|
210
|
+
|
|
211
|
+
const onOldMessages = (messages, threadType) => {
|
|
212
|
+
if (threadType === type) {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
api.listener.off('old_messages', onOldMessages);
|
|
215
|
+
resolve(messages);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
api.listener.once('connected', () => {
|
|
219
|
+
api.listener.on('old_messages', onOldMessages);
|
|
220
|
+
api.listener.requestOldMessages(type, lastMsgId);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
let response;
|
|
224
|
+
let processedResponse = {};
|
|
225
|
+
try {
|
|
226
|
+
response = await getMessages();
|
|
227
|
+
const filteredMessages = response.filter((msg) => !msg.isSelf);
|
|
228
|
+
if (filteredMessages.length > 0) {
|
|
229
|
+
let oldestTs = Infinity;
|
|
230
|
+
let newestTs = -Infinity;
|
|
231
|
+
|
|
232
|
+
const formatTimestamp = (ts) => {
|
|
233
|
+
if (!ts) return null;
|
|
234
|
+
// Create a new Date object from the timestamp
|
|
235
|
+
const date = new Date(parseInt(ts, 10));
|
|
236
|
+
// Format it to an ISO-like string for the Vietnam timezone (UTC+7)
|
|
237
|
+
// This is more machine-readable than the previous format.
|
|
238
|
+
const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Ho_Chi_Minh', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(date);
|
|
239
|
+
const p = parts.reduce((acc, part) => ({ ...acc, [part.type]: part.value }), {});
|
|
240
|
+
return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}+07:00`;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
filteredMessages.forEach((msg) => {
|
|
244
|
+
const ts = parseInt(msg.data.ts, 10);
|
|
245
|
+
msg._timestamp = formatTimestamp(ts);
|
|
246
|
+
msg._eventType = 'old_messages';
|
|
247
|
+
msg._source = 'zaloUser';
|
|
248
|
+
if (ts < oldestTs) oldestTs = ts;
|
|
249
|
+
if (ts > newestTs) newestTs = ts;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
processedResponse.count = filteredMessages.length;
|
|
253
|
+
processedResponse.oldestMessageTime = formatTimestamp(oldestTs);
|
|
254
|
+
processedResponse.newestMessageTime = formatTimestamp(newestTs);
|
|
255
|
+
}
|
|
256
|
+
processedResponse.response = filteredMessages;
|
|
257
|
+
} finally {
|
|
258
|
+
api.listener.stop(); // Always stop the listener
|
|
259
|
+
}
|
|
260
|
+
returnData.push({ json: { success: true, ...processedResponse }, pairedItem: { item: i } });
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
188
263
|
}
|
|
189
264
|
}
|
|
190
265
|
}
|
|
@@ -19,6 +19,12 @@ exports.zaloUserOperations = [
|
|
|
19
19
|
action: 'Lấy danh sách bạn bè',
|
|
20
20
|
description: 'Lấy danh sách bạn bè',
|
|
21
21
|
},
|
|
22
|
+
// {
|
|
23
|
+
// name: 'Lấy Trạng Thái Online Của Bạn Bè',
|
|
24
|
+
// value: 'getFriendOnlines',
|
|
25
|
+
// action: 'Lấy trạng thái online của bạn bè',
|
|
26
|
+
// description: 'Lấy trạng thái online và thời gian truy cập cuối của bạn bè',
|
|
27
|
+
// },
|
|
22
28
|
{
|
|
23
29
|
name: 'Chấp Nhận Lời Mời Kết Bạn',
|
|
24
30
|
value: 'acceptFriendRequest',
|
|
@@ -121,6 +127,12 @@ exports.zaloUserOperations = [
|
|
|
121
127
|
action: 'Đánh dấu tin nhắn đã đọc',
|
|
122
128
|
description: 'Đánh dấu tin nhắn là đã đọc',
|
|
123
129
|
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Lấy Trạng Thái Hoạt Động Của Người Dùng',
|
|
132
|
+
value: 'getLastOnline',
|
|
133
|
+
action: 'Lấy trạng thái hoạt động của người dùng',
|
|
134
|
+
description: 'Lấy thời gian hoạt động cuối cùng của một người dùng qua User ID',
|
|
135
|
+
},
|
|
124
136
|
{
|
|
125
137
|
name: 'Lấy Thông Tin Tài Khoản',
|
|
126
138
|
value: 'fetchAccountInfo',
|
|
@@ -133,6 +145,12 @@ exports.zaloUserOperations = [
|
|
|
133
145
|
action: 'Lấy ngữ cảnh tài khoản hiện tại',
|
|
134
146
|
description: 'Lấy ngữ cảnh (context) của phiên làm việc hiện tại',
|
|
135
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Lấy Tin Nhắn Đến Cũ của tài khoản',
|
|
150
|
+
value: 'getOldMessages',
|
|
151
|
+
action: 'Lấy tin nhắn đến cũ của tài khoản',
|
|
152
|
+
description: 'Lấy các tin nhắn cũ hơn trong một cuộc trò chuyện',
|
|
153
|
+
},
|
|
136
154
|
],
|
|
137
155
|
default: 'getUserInfo',
|
|
138
156
|
},
|
|
@@ -160,7 +178,7 @@ exports.zaloUserFields = [
|
|
|
160
178
|
displayOptions: {
|
|
161
179
|
show: {
|
|
162
180
|
resource: ['zaloUser'],
|
|
163
|
-
operation: ['removeUnreadMark', 'addUnreadMark', 'undoMessage'],
|
|
181
|
+
operation: ['removeUnreadMark', 'addUnreadMark', 'undoMessage', 'getOldMessages'],
|
|
164
182
|
},
|
|
165
183
|
},
|
|
166
184
|
options: [
|
|
@@ -176,7 +194,7 @@ exports.zaloUserFields = [
|
|
|
176
194
|
},
|
|
177
195
|
],
|
|
178
196
|
default: 0,
|
|
179
|
-
description: 'Loại cuộc trò chuyện.
|
|
197
|
+
description: 'Loại cuộc trò chuyện.',
|
|
180
198
|
},
|
|
181
199
|
{
|
|
182
200
|
displayName: 'msgId',
|
|
@@ -225,12 +243,27 @@ exports.zaloUserFields = [
|
|
|
225
243
|
'getUserInfo',
|
|
226
244
|
'undoFriendRequest',
|
|
227
245
|
'getQR',
|
|
246
|
+
'getLastOnline',
|
|
228
247
|
],
|
|
229
248
|
},
|
|
230
249
|
},
|
|
231
250
|
default: '',
|
|
232
251
|
description: 'ID của người dùng',
|
|
233
252
|
},
|
|
253
|
+
{
|
|
254
|
+
displayName: 'Last Message ID',
|
|
255
|
+
name: 'lastMsgId',
|
|
256
|
+
type: 'string',
|
|
257
|
+
displayOptions: {
|
|
258
|
+
show: {
|
|
259
|
+
resource: ['zaloUser'],
|
|
260
|
+
operation: ['getOldMessages'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
default: '',
|
|
264
|
+
placeholder: 'Để trống để lấy tin nhắn gần nhất',
|
|
265
|
+
description: 'Lấy các tin nhắn cũ hơn ID này, để trống để lấy tin nhắn mới nhất (chức năng này có thể không hoạt động như mong muốn)',
|
|
266
|
+
},
|
|
234
267
|
{
|
|
235
268
|
displayName: 'Alias Name',
|
|
236
269
|
name: 'aliasName',
|
|
@@ -13,10 +13,6 @@ const path_1 = require("path");
|
|
|
13
13
|
|
|
14
14
|
let db;
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Initializes the SQLite database using sql.js.
|
|
18
|
-
* Creates the database file if it doesn't exist and sets up the necessary tables.
|
|
19
|
-
*/
|
|
20
16
|
async function initDb() {
|
|
21
17
|
if (db)
|
|
22
18
|
return db;
|
|
@@ -50,9 +46,6 @@ async function initDb() {
|
|
|
50
46
|
return db;
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
/**
|
|
54
|
-
* Persists the database changes to the file system.
|
|
55
|
-
*/
|
|
56
49
|
async function persistDb() {
|
|
57
50
|
if (!db) return;
|
|
58
51
|
try {
|
|
@@ -90,19 +83,6 @@ async function imageMetadataGetter(filePath) {
|
|
|
90
83
|
}
|
|
91
84
|
exports.imageMetadataGetter = imageMetadataGetter;
|
|
92
85
|
|
|
93
|
-
/**
|
|
94
|
-
* Initializes and returns a Zalo API client.
|
|
95
|
-
* It prioritizes credentials in this order:
|
|
96
|
-
* 1. A user ID provided in the 'User ID' field, which is used to look up a session in the `zalo_sessions.json` file.
|
|
97
|
-
* 2. A Zalo API credential selected in the node's UI.
|
|
98
|
-
*
|
|
99
|
-
* @param {IExecuteFunctions} node The execution context of the node (`this`)
|
|
100
|
-
* @param {object} [options={}] Optional parameters.
|
|
101
|
-
* @param {boolean} [options.needsImageMetadataGetter=false] get image metadata.
|
|
102
|
-
* @param {boolean} [options.selfListen=false] listen owner events triggered.
|
|
103
|
-
* @returns {Promise<any>} A promise that resolves to an initialized Zalo API client.
|
|
104
|
-
* @throws {NodeOperationError} If no valid credentials or session can be found.
|
|
105
|
-
*/
|
|
106
86
|
async function getZaloApiClient(node, options = {}) {
|
|
107
87
|
const useSession = node.getNodeParameter('useSession', 0, false);
|
|
108
88
|
const userId = node.getNodeParameter('connectToId', 0, '');
|
|
@@ -112,7 +92,7 @@ async function getZaloApiClient(node, options = {}) {
|
|
|
112
92
|
try {
|
|
113
93
|
await initDb();
|
|
114
94
|
} catch (e) {
|
|
115
|
-
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `
|
|
95
|
+
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `Database initialization failed: ${e.message}. Please ensure a 'Zalo Login by QR' node has run successfully first.`);
|
|
116
96
|
}
|
|
117
97
|
|
|
118
98
|
try {
|
|
@@ -147,17 +127,17 @@ async function getZaloApiClient(node, options = {}) {
|
|
|
147
127
|
userAgent = sessionData.userAgent;
|
|
148
128
|
}
|
|
149
129
|
catch (e) {
|
|
150
|
-
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[
|
|
130
|
+
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[SS] Failed to decrypt session for Zalo ID: "${actualZaloId}". The file might be corrupt or the key has changed. Error: ${e.message}`);
|
|
151
131
|
}
|
|
152
132
|
}
|
|
153
133
|
else {
|
|
154
134
|
const idType = isPhoneNumber ? 'Phone number' : 'Zalo User ID';
|
|
155
|
-
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[
|
|
135
|
+
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[SS] ${idType} "${userId}" was provided, but no matching session was found.`);
|
|
156
136
|
}
|
|
157
137
|
}
|
|
158
138
|
catch (e) {
|
|
159
139
|
if (e instanceof n8n_workflow_1.NodeOperationError) throw e;
|
|
160
|
-
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[
|
|
140
|
+
throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[SS] An unexpected database error occurred: ${e.message}.`);
|
|
161
141
|
}
|
|
162
142
|
} else if (!useSession) {
|
|
163
143
|
const zaloCred = await node.getCredentials('zaloApi');
|
|
@@ -176,11 +156,7 @@ async function getZaloApiClient(node, options = {}) {
|
|
|
176
156
|
return zalo.login({ cookie, imei, userAgent });
|
|
177
157
|
}
|
|
178
158
|
exports.getZaloApiClient = getZaloApiClient;
|
|
179
|
-
|
|
180
|
-
* Saves or updates a user's session in the database.
|
|
181
|
-
* @param {object} sessionDetails - The details of the session to save.
|
|
182
|
-
* @returns {Promise<{oldCredentialId: string | null}>} - The old credential ID if it existed.
|
|
183
|
-
*/
|
|
159
|
+
|
|
184
160
|
async function saveOrUpdateSession(sessionDetails) {
|
|
185
161
|
try {
|
|
186
162
|
const { userId, name, phone, credentialId, encryptedData } = sessionDetails;
|
|
@@ -223,13 +199,7 @@ async function saveOrUpdateSession(sessionDetails) {
|
|
|
223
199
|
}
|
|
224
200
|
}
|
|
225
201
|
exports.saveOrUpdateSession = saveOrUpdateSession;
|
|
226
|
-
|
|
227
|
-
* Updates only the name and phone number for a given user ID in the database.
|
|
228
|
-
* @param {string} userId - The Zalo user ID.
|
|
229
|
-
* @param {string} name - The new display name.
|
|
230
|
-
* @param {string} phone - The new phone number.
|
|
231
|
-
* @returns {Promise<void>}
|
|
232
|
-
*/
|
|
202
|
+
|
|
233
203
|
async function updateSessionInfo(userId, name, phone) {
|
|
234
204
|
try {
|
|
235
205
|
await initDb();
|
|
@@ -255,11 +225,7 @@ async function updateSessionInfo(userId, name, phone) {
|
|
|
255
225
|
}
|
|
256
226
|
}
|
|
257
227
|
exports.updateSessionInfo = updateSessionInfo;
|
|
258
|
-
|
|
259
|
-
* Deletes a user's session from the database by user ID.
|
|
260
|
-
* @param {string} userId - The Zalo user ID of the session to delete.
|
|
261
|
-
* @returns {Promise<void>}
|
|
262
|
-
*/
|
|
228
|
+
|
|
263
229
|
async function deleteSessionByUserId(userId) {
|
|
264
230
|
try {
|
|
265
231
|
await initDb();
|
|
@@ -274,20 +240,6 @@ async function deleteSessionByUserId(userId) {
|
|
|
274
240
|
}
|
|
275
241
|
exports.deleteSessionByUserId = deleteSessionByUserId;
|
|
276
242
|
|
|
277
|
-
/**
|
|
278
|
-
* Gửi tin nhắn hoặc file đến một cuộc trò chuyện trên Telegram.
|
|
279
|
-
*
|
|
280
|
-
* @param {object} params Các tham số để gửi đến Telegram.
|
|
281
|
-
* @param {string} params.token Token của bot Telegram.
|
|
282
|
-
* @param {string} params.chatId ID của cuộc trò chuyện trên Telegram.
|
|
283
|
-
* @param {string} [params.text] Tin nhắn văn bản cần gửi.
|
|
284
|
-
* @param {Buffer} [params.binaryData] Dữ liệu nhị phân của file cần gửi.
|
|
285
|
-
* @param {string} [params.fileName] Tên của file.
|
|
286
|
-
* @param {string} [params.caption] Chú thích cho file.
|
|
287
|
-
* @param {INodeExecutionFunctions['logger']} [params.logger] Logger để ghi lại thông tin.
|
|
288
|
-
* @returns {Promise<void>}
|
|
289
|
-
* @throws {Error} Nếu gửi thất bại.
|
|
290
|
-
*/
|
|
291
243
|
async function sendToTelegram({ token, chatId, text, binaryData, fileName, caption, logger, }) {
|
|
292
244
|
if (!token || !chatId) {
|
|
293
245
|
logger === null || logger === void 0 ? void 0 : logger.warn('Telegram token và chatId là bắt buộc nhưng không được cung cấp.');
|
package/package.json
CHANGED