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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # n8n-nodes-zalo-custom
2
2
 
3
- Thư viện n8n nodes tự động hoá Zalo thông qua n8n workflow. Cho phép `Expression Zalo Credential` phù hợp khi quản lý nhiều tài khoản Zalo.
3
+ Thư viện n8n nodes tự động hoá Zalo thông qua n8n workflow. Cho phép **Expression Zalo Credential** phù hợp khi quản lý nhiều tài khoản Zalo.
4
4
 
5
5
  Hoạt động độc lập trong môi trường **n8n của bạn** — **không** dùng API của bên thứ ba hay bất kỳ phụ thuộc ngoại vi nào. Dữ liệu của bạn **luôn được giữ riêng tư và an toàn**.
6
6
 
@@ -19,24 +19,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
- | | Sự kiện về bạn bè (kết bạn, hủy bạn) |
32
- | 💬 | _NHẮN TIN_ |
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 chi tiết thành viên nhóm |
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: 'ZaloTrigger',
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 cả.',
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 (bỏ qua: Thả icon, Thu hồi, Sự kiện nhóm, Kết bạn)',
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
- if (apiInstances.has(instanceKey)) {
201
- this.logger.info(`Instance ${instanceKey} already exists, reusing...`);
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
- _user_id: context.credentialUserId,
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. Dựa trên enum ThreadType.',
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(), `[Session] Database initialization failed: ${e.message}. Please ensure a 'Zalo Login by QR' node has run successfully first.`);
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(), `[Session] Failed to decrypt session for Zalo ID: "${actualZaloId}". The file might be corrupt or the key has changed. Error: ${e.message}`);
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(), `[Session] ${idType} "${userId}" was provided, but no matching session was found.`);
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(), `[Session] An unexpected database error occurred: ${e.message}.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-zalo-custom",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "n8n nodes for Zalo automation. Send messages, manage groups, friends, and listen to events without third-party services.",
5
5
  "keywords": [
6
6
  "n8n",