n8n-nodes-zalo-custom 1.0.8 → 1.0.10

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
@@ -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 |
@@ -155,7 +159,7 @@ Khi bạn quản lý nhiều tài khoản Zalo, mỗi tài khoản sẽ có mộ
155
159
  <details>
156
160
  <summary><b>nhận sự kiện từ Zalo để phản hồi</b></summary>
157
161
 
158
- - **Lắng nghe sự kiện**: tin nhắn người dùng/nhóm, thả tim, thu hồi tin nhắn, lời mời kết bạn.
162
+ - **Lắng nghe sự kiện**: tin nhắn người dùng/nhóm, thả tim, thu hồi tin nhắn, lời mời kết bạn, đã xem tin nhắn, đang soạn tin.
159
163
  - **Cấu hình**:
160
164
  - Chấp nhận hoặc loại trừ các ID nhóm khi nhận sự kiện tin nhắn.
161
165
  - Chỉ nhận theo từ khoá hoặc loại trừ khi nhận sự kiện tin nhắn.
@@ -196,6 +200,7 @@ Khi bạn quản lý nhiều tài khoản Zalo, mỗi tài khoản sẽ có mộ
196
200
  - **Tương tác với người dùng:** Thu hồi tin nhắn, chặn/bỏ chặn, đổi tên gợi nhớ, đánh dấu đã đọc/chưa đọc.
197
201
  - **Lấy thông tin:** Lấy danh sách bạn bè, gợi ý kết bạn, thông tin chi tiết người dùng (qua User ID/SĐT), lấy mã QR.
198
202
  - **Cập nhật hồ sơ:** Thay đổi thông tin cá nhân của bạn (tên, ngày sinh, giới tính).
203
+ - **Lấy tin nhắn cũ:** Lấy danh sách các tin nhắn cũ trong khả năng.
199
204
  </details>
200
205
 
201
206
  ---
@@ -41,18 +41,18 @@ class ZaloCommunication {
41
41
 
42
42
  try {
43
43
  api = await (0, zalo_helper_1.getZaloApiClient)(this);
44
+ if (!api) {
45
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check credentials or User ID.');
46
+ }
44
47
  }
45
48
  catch (error) {
46
49
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
47
50
  }
48
- if (!api) {
49
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No API instance found. Please make sure to provide valid credentials.');
50
- }
51
51
 
52
52
  for (let i = 0; i < items.length; i++) {
53
53
  try {
54
54
  switch (resource) {
55
- // --- Logic cho Sticker ---
55
+ // --- Sticker ---
56
56
  case 'sticker':
57
57
  switch (operation) {
58
58
  case 'getStickers': {
@@ -122,7 +122,7 @@ class ZaloCommunication {
122
122
  }
123
123
  break;
124
124
 
125
- // --- Logic cho Poll ---
125
+ // --- Poll ---
126
126
  case 'poll':
127
127
  switch (operation) {
128
128
  case 'createPoll': {
@@ -223,7 +223,7 @@ class ZaloCommunication {
223
223
  }
224
224
  break;
225
225
 
226
- // --- Logic cho Tag ---
226
+ // --- Tag ---
227
227
  case 'tag':
228
228
  switch (operation) {
229
229
  case 'list': {
@@ -53,16 +53,21 @@ class ZaloGroup {
53
53
  } catch (e) { return false; }
54
54
  });
55
55
 
56
- api = await (0, zalo_helper_1.getZaloApiClient)(this, { needsImageMetadataGetter: needsImageMetadataGetter });
57
- if (!api) {
58
- throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, 'Failed to initialize Zalo API. Check credentials or User ID.');
56
+ try {
57
+ api = await (0, zalo_helper_1.getZaloApiClient)(this, {needsImageMetadataGetter: needsImageMetadataGetter});
58
+ if (!api) {
59
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, 'Failed to initialize Zalo API. Check credentials or User ID.');
60
+ }
61
+ }
62
+ catch (error) {
63
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
59
64
  }
60
65
 
61
66
  for (let i = 0; i < items.length; i++) {
62
67
  try {
63
68
  const resource = this.getNodeParameter('resource', i);
64
69
  const operation = this.getNodeParameter('operation', i);
65
- this.logger.info(`[GROUP] ${operation} - Image ${needsImageMetadataGetter}`)
70
+ // this.logger.info(`[GROUP] ${operation} - Image ${needsImageMetadataGetter}`)
66
71
 
67
72
  if (resource === 'zaloGroup') {
68
73
  switch (operation) {
@@ -263,6 +268,12 @@ class ZaloGroup {
263
268
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
264
269
  break;
265
270
  }
271
+ case 'getGroupLinkInfo': {
272
+ const groupLink = this.getNodeParameter('groupLink', i);
273
+ const response = await api.getGroupLinkInfo({ link: groupLink });
274
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
275
+ break;
276
+ }
266
277
  case 'joinGroupLink': {
267
278
  const groupLink = this.getNodeParameter('groupLink', i);
268
279
  const response = await api.joinGroupLink(groupLink);
@@ -282,6 +293,21 @@ class ZaloGroup {
282
293
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
283
294
  break;
284
295
  }
296
+ case 'deleteMessage': {
297
+ const groupId = this.getNodeParameter('groupId', i);
298
+ const uidFrom = this.getNodeParameter('userId', i);
299
+ const msgId = this.getNodeParameter('msgId', i);
300
+ const cliMsgId = this.getNodeParameter('cliMsgId', i);
301
+ const onlyMe = this.getNodeParameter('onlyMe', i, true);
302
+ const dest = {
303
+ data: { cliMsgId, msgId, uidFrom },
304
+ threadId: groupId,
305
+ type: 1, // ThreadType.Group
306
+ };
307
+ const response = await api.deleteMessage(dest, onlyMe);
308
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
309
+ break;
310
+ }
285
311
  }
286
312
  }
287
313
  }
@@ -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
  ];
@@ -43,6 +43,8 @@ const path = __importStar(require("path"));
43
43
  const zalo_helper_1 = require("../utils/zalo.helper");
44
44
  const crypto_helper_1 = require("../utils/crypto.helper");
45
45
  const axios_1 = __importDefault(require("axios"));
46
+ const https_proxy_agent_1 = require("https-proxy-agent");
47
+ const node_fetch_1 = __importDefault(require("node-fetch"));
46
48
 
47
49
  class ZaloLoginByQr {
48
50
  constructor() {
@@ -73,7 +75,7 @@ class ZaloLoginByQr {
73
75
  type: 'string',
74
76
  default: '',
75
77
  placeholder: 'https://user:pass@host:port',
76
- description: 'HTTP proxy to use for Zalo API requests',
78
+ description: 'HTTP proxy to use for Zalo API requests (proxy can only be saved once for a custom credential)',
77
79
  },
78
80
  // {
79
81
  // displayName: 'Delete Zalo Credential Duplicate UserId',
@@ -116,12 +118,28 @@ class ZaloLoginByQr {
116
118
  }
117
119
  async execute() {
118
120
  const returnData = [];
119
- let proxy = this.getNodeParameter('proxy', 0, '');
120
- if (proxy && !proxy.toLowerCase().startsWith('http')) {
121
- this.logger.warn(`Proxy không hợp lệ: "${proxy}"`);
122
- proxy = '';
123
- }
124
- const timeout = 30;
121
+ let proxy = this.getNodeParameter('proxy', 0, '');
122
+ if (proxy) {
123
+ if (!proxy.toLowerCase().startsWith('http')) {
124
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Proxy không hợp lệ: "${proxy}" (http(s)://user:pass@host:port)`);
125
+ }
126
+ try {
127
+ const response = await axios_1.default.get('https://api.ipify.org', {
128
+ httpsAgent: new https_proxy_agent_1.HttpsProxyAgent(proxy),
129
+ timeout: 5000,
130
+ });
131
+ this.logger.info(`Use Proxy IP Public: ${response.data}`);
132
+ } catch (error) {
133
+ this.logger.error(`Kiểm tra proxy thất bại: ${error.message}`);
134
+ let errorMessage = error.message;
135
+ if (error.response) {
136
+ errorMessage += ` - ${JSON.stringify(error.response.data)}`;
137
+ }
138
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Proxy không hợp lệ hoặc không thể kết nối. Lỗi: ${errorMessage} (http(s)://user:pass@host:port)`);
139
+ }
140
+ }
141
+
142
+ const timeout = 45; // telegram
125
143
  const fileName = 'zalo-qr-code.png';
126
144
  const deleteOldZaloApi = this.getNodeParameter('deleteOldZaloApi', 0, false); // bỏ vì xoá xong gây ra lỗi k lưu được
127
145
  const sendToTelegram = this.getNodeParameter('sendToTelegram', 0, false);
@@ -133,41 +151,18 @@ class ZaloLoginByQr {
133
151
  chatId: telegramChatId,
134
152
  logger: this.logger,
135
153
  };
136
- let n8nCredential;
137
- try {
138
- n8nCredential = await this.getCredentials('n8nZaloApi');
139
- }
140
- catch (error) {
141
- }
142
- const selectedCredential = n8nCredential;
143
- if (selectedCredential) {
144
- this.logger.info('Using n8n account credential');
145
- }
146
- else {
147
- this.logger.info('No credentials provided, will generate QR code for login');
148
- }
154
+ let selectedCredential = await this.getCredentials('n8nZaloApi');
149
155
  try {
150
156
  const zaloOptions = {
151
157
  selfListen: true,
152
- logging: true,
158
+ logging: false,
153
159
  };
154
160
  if (proxy) {
155
- zaloOptions.proxy = proxy;
156
- }
157
- let zalo;
158
- if (selectedCredential) {
159
- this.logger.info('Using existing Zalo credentials');
160
- zalo = new zca_js_1.Zalo(zaloOptions);
161
- this.logger.info('Using n8n credential to get Zalo credentials');
162
- const n8nApiKey = selectedCredential.apiKey;
163
- const n8nUrl = selectedCredential.url || 'http://localhost:5678';
164
- this.logger.info(`Using n8n API at ${n8nUrl} with API key ${n8nApiKey ? 'provided' : 'not provided'}`);
165
- this.logger.info('n8n credential support is not fully implemented yet. Will use QR code login.');
166
- zalo = new zca_js_1.Zalo(zaloOptions);
167
- }
168
- else {
169
- zalo = new zca_js_1.Zalo(zaloOptions);
161
+ zaloOptions.agent = new https_proxy_agent_1.HttpsProxyAgent(proxy);
162
+ zaloOptions.polyfill = node_fetch_1.default;
170
163
  }
164
+ let zalo = new zca_js_1.Zalo(zaloOptions);
165
+
171
166
  this.logger.info('Starting Zalo QR login process...');
172
167
  let userDisplayName = '';
173
168
  let userAvatar = '';
@@ -187,12 +182,6 @@ class ZaloLoginByQr {
187
182
  userImei = imei;
188
183
  userUserAgent = userAgent;
189
184
  userZaloUserId = zaloUserId;
190
- // this.logger.info('=== ZALO CREDENTIALS ===');
191
- // this.logger.info(`Cookie: ${cookie ? `Received (length: ${typeof cookie === 'string' ? cookie.length : (Array.isArray(cookie) ? cookie.length : 'unknown')})` : 'None'}`);
192
- // this.logger.info(`IMEI: ${imei ? imei : 'None'}`);
193
- // this.logger.info(`User Agent: ${userAgent ? userAgent : 'None'}`);
194
- // this.logger.info(`Zalo User ID: ${zaloUserId ? zaloUserId : 'None'}`);
195
- // this.logger.info('=== END CREDENTIALS ===');
196
185
  };
197
186
  const setupEventListeners = (api) => {
198
187
  this.logger.info('Setting up event listeners to get credentials');
@@ -271,7 +260,7 @@ class ZaloLoginByQr {
271
260
  }).catch(e => this.logger.error(`Không thể gửi thông báo timeout đến Telegram: ${e.message}`));
272
261
  }
273
262
  this.logger.warn('QR code expired. Please try again.');
274
- break; // This event often precedes a timeout error, so a log is sufficient.
263
+ break;
275
264
  case 2:
276
265
  this.logger.info('=== QR CODE SCANNED ===');
277
266
  if (qrEvent === null || qrEvent === void 0 ? void 0 : qrEvent.data) {
@@ -309,7 +298,6 @@ class ZaloLoginByQr {
309
298
  }).catch(e => this.logger.error(`Không thể gửi thông báo đăng nhập thành công đến Telegram: ${e.message}`));
310
299
  }
311
300
  if (cookie && cookie.length > 0 && imei && userAgent) {
312
- // Use an async IIFE to handle the async login and subsequent actions
313
301
  (async () => {
314
302
  try {
315
303
  this.logger.info('Login trực tiếp để lấy UID...');
@@ -353,10 +341,9 @@ class ZaloLoginByQr {
353
341
  const ports = [5678];
354
342
  const createCredentialOnPort = async (port) => {
355
343
  const n8nApi = await this.getCredentials('n8nZaloApi');
356
- const n8nApiUrl = n8nApi.url;
357
- const fullApiUrl = `${n8nApiUrl}/api/v1/credentials`;
358
344
  const n8nApiKey = n8nApi.apiKey;
359
- // this.logger.info(`Trying to create credential via n8n API at ${fullApiUrl}`);
345
+ const n8nApiUrl = n8nApi.url.replace(/\/$/, '');
346
+ const fullApiUrl = `${n8nApiUrl}/api/v1/credentials`;
360
347
  try {
361
348
  const response = await axios_1.default.post(fullApiUrl, credentialApiData, {
362
349
  headers: {
@@ -364,7 +351,7 @@ class ZaloLoginByQr {
364
351
  'X-N8N-API-KEY': n8nApiKey
365
352
  },
366
353
  });
367
- // this.logger.info('Credential created successfully via n8n API');
354
+
368
355
  if (response.data && response.data.id) {
369
356
  this.logger.info(`Credential ID: ${response.data.id}`);
370
357
  createdCredentialId = response.data.id;
@@ -372,7 +359,7 @@ class ZaloLoginByQr {
372
359
  try {
373
360
  if (localUserZaloUserId && localUserZaloUserId !== 'unknown') {
374
361
  const encryptionKey = localUserZaloUserId.repeat(3);
375
- const sessionDataToEncrypt = { cookie, imei, userAgent };
362
+ const sessionDataToEncrypt = { cookie, imei, userAgent, proxy };
376
363
  const encryptedData = (0, crypto_helper_1.encrypt)(sessionDataToEncrypt, encryptionKey);
377
364
  const sessionDetails = {
378
365
  userId: localUserZaloUserId,
@@ -505,9 +492,7 @@ class ZaloLoginByQr {
505
492
  json: {
506
493
  success: true,
507
494
  state: '',
508
- message: selectedCredential
509
- ? 'Using n8n account credential. QR code generated successfully.'
510
- : 'QR code generated successfully. Scan with Zalo app to login.',
495
+ message: selectedCredential ? 'Using n8n account credential. QR code generated successfully.' : 'Missing n8n api',
511
496
  fileName,
512
497
  usingExistingCredential: !!selectedCredential,
513
498
  credentialType: selectedCredential ? 'n8nZaloApi' : null,
@@ -561,10 +561,8 @@ class ZaloSendMessage {
561
561
  const returnData = [];
562
562
  const items = this.getInputData();
563
563
  try {
564
- // Use the new helper function to get the API client
565
564
  api = await (0, zalo_helper_1.getZaloApiClient)(this, { needsImageMetadataGetter: true });
566
565
  if (!api) {
567
- // The helper function will throw an error, but as a fallback:
568
566
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check credentials or User ID.');
569
567
  }
570
568
  }
@@ -577,7 +575,6 @@ class ZaloSendMessage {
577
575
  },
578
576
  });
579
577
  return [returnData]
580
- // throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
581
578
  }
582
579
  for (let i = 0; i < items.length; i++) {
583
580
  try {
@@ -595,7 +592,6 @@ class ZaloSendMessage {
595
592
  const attachmentUrls = this.getNodeParameter('attachmentUrls', i, '');
596
593
  const reactionParam = this.getNodeParameter('reaction', i, {});
597
594
 
598
- // return json
599
595
  const returnJson = {
600
596
  threadId,
601
597
  threadType: type
@@ -677,15 +673,12 @@ class ZaloSendMessage {
677
673
  messageContent.attachments = [];
678
674
  for (const url of urls) {
679
675
  try {
680
- // Validate URL format
681
676
  new URL(url);
682
- // Download file and get its local path
683
677
  const fileData = await (0, helper_1.saveFile)(url);
684
678
  messageContent.attachments.push(fileData);
685
679
  this.logger.info(`Successfully prepared attachment from URL: ${url}`);
686
680
  }
687
681
  catch (e) {
688
- // Log a warning and skip if the URL is invalid or the file can't be downloaded
689
682
  this.logger.warn(`Skipping invalid or inaccessible attachment URL: ${url}. Error: ${e.message}`);
690
683
  }
691
684
  }
@@ -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,22 +197,47 @@ 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, telegramChatId, telegramOptions;
204
+ if (continueOnFail) {
205
+ const tokenOverride = this.getNodeParameter('telegramTokenOverride', 0, '');
206
+ const chatIdOverride = this.getNodeParameter('telegramChatIdOverride', 0, '');
207
+ const credsToken = credentials ? credentials.telegramToken : '';
208
+ const credsChatId = credentials ? credentials.telegramChatId : '';
209
+ telegramToken = tokenOverride || credsToken;
210
+ telegramChatId = chatIdOverride || credsChatId;
211
+ if (!telegramToken || !telegramChatId) {
212
+ 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';
213
+ } else {
214
+ telegramOptions = {
215
+ token: telegramToken,
216
+ chatId: telegramChatId,
217
+ logger: this.logger,
218
+ };
219
+ }
202
220
  }
221
+ const sendTelegramNotification = (message) => {
222
+ if (telegramOptions) {
223
+ (0, zalo_helper_1.sendToTelegram)({
224
+ ...telegramOptions,
225
+ text: message,
226
+ }).catch(e => this.logger.error(`Failed to send Telegram notification: ${e.message}`));
227
+ }
228
+ };
203
229
 
204
230
  let api = apiInstances.get(instanceKey);
205
231
  let closeFunction = async () => { };
206
-
207
232
  try {
233
+ if (apiInstances.has(instanceKey)) {
234
+ this.logger.info(`Instance ${instanceKey} already exists, reusing...`);
235
+ }
208
236
  if (!api) {
209
237
  api = await (0, zalo_helper_1.getZaloApiClient)(this, { selfListen });
210
238
  if (!api) {
211
239
  throw new Error('No API instance found. Please make sure to provide valid credentials.');
212
240
  }
213
-
214
241
  try {
215
242
  const fetchedInfo = await api.fetchAccountInfo();
216
243
  if (fetchedInfo && fetchedInfo.profile) {
@@ -245,7 +272,6 @@ class ZaloTrigger {
245
272
  catch (err) {
246
273
  this.logger.error(`[Zalo ${mode}] Failed to fetch account info: ${err.message}`);
247
274
  }
248
-
249
275
  apiInstances.set(instanceKey, api);
250
276
  }
251
277
 
@@ -254,18 +280,18 @@ class ZaloTrigger {
254
280
  if (!data?.isSelf) {
255
281
  context.logger.info(`[${apiInstances.size}] [Zalo ${context.mode} received] ${context.credentialPhone}: ${eventType}`);
256
282
  }
257
-
258
283
  const dataWithContext = {
259
284
  ...data,
260
285
  _timestamp: new Date().toISOString(),
261
286
  _instanceKey: context.instanceKey,
262
287
  _name: context.credentialName,
263
288
  _phone: context.credentialPhone,
264
- _user_id: context.credentialUserId,
289
+ _userId: context.credentialUserId,
290
+ _continueOnFail: context.continueOnFail,
291
+ _errorNotification: context.errorNotification,
265
292
  _eventType: eventType,
266
293
  _source: 'zalo_trigger',
267
294
  };
268
-
269
295
  const webhookData = this.getWorkflowStaticData('node');
270
296
  if (!webhookData[context.instanceKey]) {
271
297
  webhookData[context.instanceKey] = {};
@@ -281,11 +307,12 @@ class ZaloTrigger {
281
307
 
282
308
  emitEvent(dataWithContext);
283
309
  };
284
-
285
310
  if (context.eventTypes.includes('message_user') || context.eventTypes.includes('message_group')) {
286
311
  api.listener.on('message', async (message) => {
287
312
  const eventType = message.type === threadTypeUser ? 'message_user' : 'message_group';
288
-
313
+ if (!context.eventTypes.includes(eventType)) {
314
+ return;
315
+ }
289
316
  if (context.threadIdFiltering) {
290
317
  if (context.listenOnlyThreads.length > 0 && !context.listenOnlyThreads.includes(message.threadId)) {
291
318
  context.logger.info(`[zalo listen] ${context.instanceKey} threadId: ${message.threadId} (not in listen-only list)`);
@@ -296,11 +323,9 @@ class ZaloTrigger {
296
323
  return;
297
324
  }
298
325
  }
299
-
300
326
  if (context.keywordFiltering && (context.listenOnlyKeywords.length > 0 || context.ignoreKeywords.length > 0)) {
301
327
  let messageText = '';
302
328
  const content = message?.data?.content;
303
-
304
329
  if (typeof content === 'string') {
305
330
  messageText = content;
306
331
  }
@@ -318,7 +343,6 @@ class ZaloTrigger {
318
343
  }
319
344
  }
320
345
  }
321
-
322
346
  const lower = (messageText || '').toLowerCase();
323
347
  if (context.listenOnlyKeywords.length > 0 && !context.listenOnlyKeywords.some(k => lower.includes(k))) {
324
348
  context.logger.info(`[zalo listen] ${context.instanceKey} keyword: ${lower} in ${message.threadId} ignored (no listen-only keyword found)`);
@@ -329,46 +353,69 @@ class ZaloTrigger {
329
353
  return;
330
354
  }
331
355
  }
332
-
333
- if (context.eventTypes.includes(eventType)) {
334
- await handleEvent(message, eventType);
335
- }
356
+ await handleEvent(message, eventType);
336
357
  });
337
358
  }
338
-
339
359
  if (context.eventTypes.includes('reaction')) {
340
360
  api.listener.on('reaction', async (reaction) => {
341
361
  if (context.ignoreSelfEvents && reaction.isSelf) return;
342
362
  await handleEvent(reaction, 'reaction');
343
363
  });
344
364
  }
345
-
346
365
  if (context.eventTypes.includes('undo')) {
347
366
  api.listener.on('undo', async (undo) => {
348
367
  if (context.ignoreSelfEvents && undo.isSelf) return;
349
368
  await handleEvent(undo, 'undo');
350
369
  });
351
370
  }
352
-
353
371
  if (context.eventTypes.includes('group_event')) {
354
372
  api.listener.on('group_event', async (groupEvent) => {
355
373
  if (context.ignoreSelfEvents && groupEvent.isSelf) return;
356
374
  await handleEvent(groupEvent, 'group_event');
357
375
  });
358
376
  }
359
-
360
377
  if (context.eventTypes.includes('friend_request')) {
361
378
  api.listener.on('friend_event', async (friendEvent) => {
362
379
  if (context.ignoreSelfEvents && friendEvent.isSelf) return;
363
380
  await handleEvent(friendEvent, 'friend_request');
364
381
  });
365
382
  }
383
+ if (context.eventTypes.includes('seen_message')) {
384
+ api.listener.on('seen_messages', async (seenMessages) => {
385
+ for (const seen of seenMessages) {
386
+ if (seen.type === threadTypeUser && !seen.isSelf) {
387
+ await handleEvent(seen, 'seen_message');
388
+ }
389
+ }
390
+ });
391
+ }
392
+ if (context.eventTypes.includes('typing')) {
393
+ api.listener.on('typing', async (typing) => {
394
+ if (seen.type === threadTypeUser && !seen.isSelf) {
395
+ await handleEvent(typing, 'typing');
396
+ }
397
+ });
398
+ }
366
399
 
367
- // Start listening
368
- api.listener.start();
400
+ api.listener.on('disconnected', (code, reason) => {
401
+ context.logger.warn(`[Zalo ${context.mode}] disconnected: ${currentPhone} - ${code}: "${reason || 'No reason'}". Attempting to reconnect...`);
402
+ if (reason !== 'NORMAL_CLOSURE') {
403
+ sendTelegramNotification(`[Zalo ${context.mode}] disconnected: ${currentPhone}: err: "${reason || 'No reason'}". Attempting to reconnect...`);
404
+ }
405
+ });
406
+ api.listener.on('closed', (code, reason) => {
407
+ context.logger.warn(`[Zalo ${context.mode}] closed: ${currentPhone} - ${code}: "${reason || 'No reason'}" (stop trigger)`);
408
+ if (reason !== 'NORMAL_CLOSURE') {
409
+ sendTelegramNotification(`[Zalo ${context.mode}] closed: ${currentPhone}: err: "${reason || 'No reason'}" (stop trigger)`);
410
+ }
411
+ });
412
+ api.listener.on('error', (error) => {
413
+ const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
414
+ context.logger.warn(`[Zalo ${context.mode}] WebSocket Error: ${currentPhone} - ${errorMessage}`);
415
+ });
416
+ api.listener.start({ retryOnClose: true });
369
417
  this.logger.info(`[${apiInstances.size}] [Zalo ${mode}] Listening: ${currentPhone} - ${currentName} (${context.eventTypes})`);
370
418
 
371
- // manual within 60s
372
419
  const manualTriggerFunction = async () => {
373
420
  if (mode !== 'manual') return true;
374
421
  return await new Promise(async (resolve, reject) => {
@@ -466,21 +513,8 @@ class ZaloTrigger {
466
513
  catch (error) {
467
514
  const continueOnFail = this.getNodeParameter('continueOnFail', 0, false);
468
515
  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
-
516
+ const errorMessage = `⚠️ **Lỗi Kích Hoạt Zalo Trigger**\n\n- Tài khoản: ${currentName}\n- SĐT: ${currentPhone}\n- do: ${error.message} (bỏ qua node này tiếp tục active workflow)`;
517
+ sendTelegramNotification(errorMessage);
484
518
  return {
485
519
  closeFunction: async () => { },
486
520
  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");
@@ -46,9 +47,14 @@ class ZaloUser {
46
47
  const items = this.getInputData();
47
48
  const returnData = [];
48
49
 
49
- api = await (0, zalo_helper_1.getZaloApiClient)(this, {});
50
- if (!api) {
51
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check credentials or User ID.');
50
+ try {
51
+ api = await (0, zalo_helper_1.getZaloApiClient)(this, {});
52
+ if (!api) {
53
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check credentials or User ID.');
54
+ }
55
+ }
56
+ catch (error) {
57
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
52
58
  }
53
59
 
54
60
  for (let i = 0; i < items.length; i++) {
@@ -118,6 +124,17 @@ class ZaloUser {
118
124
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
119
125
  break;
120
126
  }
127
+ case 'getFriendOnlines': { // err
128
+ const response = await api.getFriendOnlines();
129
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
130
+ break;
131
+ }
132
+ case 'getLastOnline': {
133
+ const userId = this.getNodeParameter('userId', i);
134
+ const response = await api.lastOnline(userId);
135
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
136
+ break;
137
+ }
121
138
  case 'findUser': {
122
139
  const phoneNumber = this.getNodeParameter('phoneNumber', i);
123
140
  const response = await api.findUser(phoneNumber);
@@ -185,6 +202,69 @@ class ZaloUser {
185
202
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
186
203
  break;
187
204
  }
205
+ case 'getOldMessages': {
206
+ const type = this.getNodeParameter('threadType', i);
207
+ const lastMsgId = this.getNodeParameter('lastMsgId', i, null);
208
+ api.listener.start({ retryOnClose: false });
209
+ const getMessages = () =>
210
+ new Promise((resolve, reject) => {
211
+ const timeout = setTimeout(() => {
212
+ api.listener.off('old_messages', onOldMessages);
213
+ reject(new Error('Timeout: Did not receive old messages within 30 seconds.'));
214
+ }, 30000);
215
+
216
+ const onOldMessages = (messages, threadType) => {
217
+ if (threadType === type) {
218
+ clearTimeout(timeout);
219
+ api.listener.off('old_messages', onOldMessages);
220
+ resolve(messages);
221
+ }
222
+ };
223
+ api.listener.once('connected', () => {
224
+ api.listener.on('old_messages', onOldMessages);
225
+ api.listener.requestOldMessages(type, lastMsgId);
226
+ });
227
+ });
228
+ let response;
229
+ let processedResponse = {};
230
+ try {
231
+ response = await getMessages();
232
+ const filteredMessages = response.filter((msg) => !msg.isSelf);
233
+ if (filteredMessages.length > 0) {
234
+ let oldestTs = Infinity;
235
+ let newestTs = -Infinity;
236
+
237
+ const formatTimestamp = (ts) => {
238
+ if (!ts) return null;
239
+ // Create a new Date object from the timestamp
240
+ const date = new Date(parseInt(ts, 10));
241
+ // Format it to an ISO-like string for the Vietnam timezone (UTC+7)
242
+ // This is more machine-readable than the previous format.
243
+ 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);
244
+ const p = parts.reduce((acc, part) => ({ ...acc, [part.type]: part.value }), {});
245
+ return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}+07:00`;
246
+ };
247
+
248
+ filteredMessages.forEach((msg) => {
249
+ const ts = parseInt(msg.data.ts, 10);
250
+ msg._timestamp = formatTimestamp(ts);
251
+ msg._eventType = 'old_messages';
252
+ msg._source = 'zaloUser';
253
+ if (ts < oldestTs) oldestTs = ts;
254
+ if (ts > newestTs) newestTs = ts;
255
+ });
256
+
257
+ processedResponse.count = filteredMessages.length;
258
+ processedResponse.oldestMessageTime = formatTimestamp(oldestTs);
259
+ processedResponse.newestMessageTime = formatTimestamp(newestTs);
260
+ }
261
+ processedResponse.response = filteredMessages;
262
+ } finally {
263
+ api.listener.stop(); // Always stop the listener
264
+ }
265
+ returnData.push({ json: { success: true, ...processedResponse }, pairedItem: { item: i } });
266
+ break;
267
+ }
188
268
  }
189
269
  }
190
270
  }
@@ -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',
@@ -8,15 +8,13 @@ const fs_1 = require("fs");
8
8
  const axios_1 = require("axios");
9
9
  const FormData = require("form-data");
10
10
  const sql_js_1 = require("sql.js");
11
+ const https_proxy_agent_1 = require("https-proxy-agent");
12
+ const node_fetch_1 = __importDefault(require("node-fetch"));
11
13
  const crypto_helper_1 = require("./crypto.helper");
12
14
  const path_1 = require("path");
13
15
 
14
16
  let db;
15
17
 
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
18
  async function initDb() {
21
19
  if (db)
22
20
  return db;
@@ -50,9 +48,6 @@ async function initDb() {
50
48
  return db;
51
49
  }
52
50
 
53
- /**
54
- * Persists the database changes to the file system.
55
- */
56
51
  async function persistDb() {
57
52
  if (!db) return;
58
53
  try {
@@ -90,29 +85,16 @@ async function imageMetadataGetter(filePath) {
90
85
  }
91
86
  exports.imageMetadataGetter = imageMetadataGetter;
92
87
 
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
88
  async function getZaloApiClient(node, options = {}) {
107
89
  const useSession = node.getNodeParameter('useSession', 0, false);
108
90
  const userId = node.getNodeParameter('connectToId', 0, '');
109
- const { needsImageMetadataGetter = false, selfListen = false } = options;
110
- let cookie, imei, userAgent, sessionInfo = null, actualZaloId = '';
91
+ const { needsImageMetadataGetter = false, selfListen = false, logging = false } = options;
92
+ let cookie, imei, userAgent, sessionInfo, proxy = null, actualZaloId = '';
111
93
  if (useSession && userId) {
112
94
  try {
113
95
  await initDb();
114
96
  } 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.`);
97
+ 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
98
  }
117
99
 
118
100
  try {
@@ -145,42 +127,58 @@ async function getZaloApiClient(node, options = {}) {
145
127
  cookie = sessionData.cookie;
146
128
  imei = sessionData.imei;
147
129
  userAgent = sessionData.userAgent;
130
+ proxy = sessionData.proxy;
148
131
  }
149
132
  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}`);
133
+ 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
134
  }
152
135
  }
153
136
  else {
154
137
  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.`);
138
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[SS] ${idType} "${userId}" was provided, but no matching session was found.`);
156
139
  }
157
140
  }
158
141
  catch (e) {
159
142
  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}.`);
143
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[SS] An unexpected database error occurred: ${e.message}.`);
161
144
  }
162
145
  } else if (!useSession) {
163
146
  const zaloCred = await node.getCredentials('zaloApi');
164
147
  cookie = JSON.parse(zaloCred.cookie);
165
148
  imei = zaloCred.imei;
166
149
  userAgent = zaloCred.userAgent;
150
+ proxy = zaloCred.proxy;
151
+ }
152
+
153
+ const zaloOptions = {
154
+ logging,
155
+ selfListen,
156
+ settings: {
157
+ features: {
158
+ socket: {
159
+ close_and_retry_codes: [1006, 1000, 3000, 3003],
160
+ retries: {
161
+ "1006": {
162
+ max: 10,
163
+ times: [5000, 10000, 30000],
164
+ },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ };
170
+ if (proxy) {
171
+ zaloOptions.agent = new https_proxy_agent_1.HttpsProxyAgent(proxy);
172
+ zaloOptions.polyfill = node_fetch_1.default;
167
173
  }
168
- const zaloOptions = {};
169
174
  if (needsImageMetadataGetter) {
170
175
  zaloOptions.imageMetadataGetter = imageMetadataGetter;
171
176
  }
172
- if (selfListen) {
173
- zaloOptions.selfListen = selfListen;
174
- }
175
177
  const zalo = new zca_js_1.Zalo(zaloOptions);
176
178
  return zalo.login({ cookie, imei, userAgent });
177
179
  }
178
180
  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
- */
181
+
184
182
  async function saveOrUpdateSession(sessionDetails) {
185
183
  try {
186
184
  const { userId, name, phone, credentialId, encryptedData } = sessionDetails;
@@ -223,13 +221,7 @@ async function saveOrUpdateSession(sessionDetails) {
223
221
  }
224
222
  }
225
223
  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
- */
224
+
233
225
  async function updateSessionInfo(userId, name, phone) {
234
226
  try {
235
227
  await initDb();
@@ -255,11 +247,7 @@ async function updateSessionInfo(userId, name, phone) {
255
247
  }
256
248
  }
257
249
  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
- */
250
+
263
251
  async function deleteSessionByUserId(userId) {
264
252
  try {
265
253
  await initDb();
@@ -274,23 +262,9 @@ async function deleteSessionByUserId(userId) {
274
262
  }
275
263
  exports.deleteSessionByUserId = deleteSessionByUserId;
276
264
 
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
265
  async function sendToTelegram({ token, chatId, text, binaryData, fileName, caption, logger, }) {
292
266
  if (!token || !chatId) {
293
- 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.');
267
+ logger === null || logger === void 0 ? void 0 : logger.warn('Telegram token và chatId là bắt buộc');
294
268
  return;
295
269
  }
296
270
  const baseUrl = `https://api.telegram.org/bot${token}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-zalo-custom",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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",
@@ -51,6 +51,8 @@
51
51
  "dependencies": {
52
52
  "axios": "^1.8.4",
53
53
  "express": "^5.1.0",
54
+ "https-proxy-agent": "^7.0.5",
55
+ "node-fetch": "^3.3.2",
54
56
  "zca-js": "^2.0.4",
55
57
  "image-size": "^1.1.1",
56
58
  "sql.js": "^1.10.3"