n8n-nodes-zalo-custom 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Hoạt động độc lập trong môi trường **n8n của bạn** — **khôn
6
6
 
7
7
  Đây là một dự án **mã nguồn mở**, chúng tôi khuyến khích cộng đồng cùng đóng góp và phát triển thêm các tính năng mới.
8
8
 
9
- ## 📱 Liên hệ & Hỗ trợ
9
+ ## 📱 Liên hệ & Báo lỗi
10
10
 
11
11
  **Github:** [codedao12](https://github.com/codedao12)
12
12
  **Email:** codedao12@gmail.com
@@ -17,53 +17,56 @@ Vui lòng liên hệ để yêu cầu tính năng mới hoặc báo lỗi.
17
17
 
18
18
  ## ✨ Các Tính Năng Chính
19
19
 
20
- | 💯 | Tính Năng |
21
- |:----:|:--------------------------------------------------|
22
- | 🔑 | **_XÁC THỰC & KẾT NỐI_** |
23
- | ✓ | Đăng nhập bằng Mã QR |
24
- | ✓✓ | Expression Zalo Credential |
25
- | ✓ | Hỗ trợ đăng nhập nhiều tài khoản, proxy |
26
- | ✓✓ | Đăng nhập, thông báo qua telegram |
27
- | ⚡️ | **_TRIGGER_** |
28
- | ✓ | Sự kiện tin nhắn mới (lọc theo từ khóa, nhóm) |
29
- | ✓ | Sự kiện thu hồi tin nhắn, thả cảm xúc |
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
- | ✓✓ | 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_** |
35
- | ✓ | Gửi tin nhắn (Văn bản, Ảnh, Sticker) |
36
- | ✓ | Trả lời tin nhắn (Quote) |
37
- | ✓ | Gắn thẻ (Tag) thành viên trong nhóm |
38
- | ✓✓ | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
39
- | ✓ | Thu hồi tin nhắn đã gửi |
40
- | ✓✓ | Lấy danh sách tin nhắn cũ |
41
- | ✓ | Mô phỏng trạng thái "Đang soạn tin..." |
42
- | 👤 | **_QUẢN LÝ TÀI KHOẢN & BẠN BÈ_** |
43
- | ✓ | Gửi / Hủy lời mời kết bạn |
44
- | ✓ | Chấp nhận / Từ chối lời mời kết bạn |
45
- | ✓ | Hủy kết bạn (xóa bạn) |
46
- | ✓ | Lấy danh sách bạn bè & lời mời đã gửi |
47
- | | Tìm người dùng bằng SĐT hoặc User ID |
48
- | | Lấy thông tin chi tiết người dùng |
49
- | | Cập nhật thông tin cá nhân (Tên, Giới tính,...) |
50
- | ✓ | Chặn / Bỏ chặn người dùng |
51
- | 👥 | **_QUẢN NHÓM_** |
52
- | ✓ | Tạo nhóm mới |
53
- | ✓ | Thêm / Xóa thành viên khỏi nhóm |
54
- | ✓✓ | Xóa tin nhắn thành viên |
55
- | ✓ | Giải tán nhóm |
56
- | | Thay đổi tên & ảnh đại diện nhóm |
57
- | ✓ | Bổ nhiệm quyền Phó nhóm |
58
- | ✓✓ | Chuyển quyền Trưởng nhóm |
59
- | ✓ | Lấy danh sách tất cả các nhóm đã tham gia |
60
- | ✓ | Lấy thông tin nhóm (từ ID hoặc link) |
61
- | | Tham gia nhóm bằng link / Rời nhóm |
62
- | ✓ | Tạo ghi chú (Note) trong nhóm |
63
- | 🎨 | **_KHÁC_** |
64
- | ✓ | Tạo bình chọn (Poll) trong nhóm |
65
- | ✓ | Quản thẻ phân loại (Tag) |
66
- | | Tìm kiếm sticker |
20
+ | 💯 | Tính Năng |
21
+ |:----:|:----------------------------------------------------|
22
+ | 🔑 | **_XÁC THỰC & KẾT NỐI_** |
23
+ | ✓ | Đăng nhập bằng Mã QR |
24
+ | ✓✓ | Expression Zalo Credential |
25
+ | ✓ | Hỗ trợ đăng nhập nhiều tài khoản, proxy |
26
+ | ✓✓ | Đăng nhập, thông báo qua telegram |
27
+ | ⚡️ | **_TRIGGER_** |
28
+ | ✓ | Sự kiện tin nhắn mới (lọc theo từ khóa, nhóm) |
29
+ | ✓ | Sự kiện thu hồi tin nhắn, thả cảm xúc |
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
+ | ✓✓ | 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_** |
35
+ | ✓ | Gửi tin nhắn (Văn bản, Ảnh, Sticker) |
36
+ | ✓ | Trả lời tin nhắn (Quote) |
37
+ | ✓ | Gắn thẻ (Tag) thành viên trong nhóm |
38
+ | ✓✓ | Thả/Gỡ cảm xúc (Reaction) vào tin nhắn |
39
+ | ✓ | Thu hồi tin nhắn đã gửi |
40
+ | ✓✓ | Lấy danh sách tin nhắn cũ |
41
+ | ✓ | Mô phỏng trạng thái "Đang soạn tin..." |
42
+ | 👤 | **_QUẢN LÝ TÀI KHOẢN & BẠN BÈ_** |
43
+ | ✓ | Gửi / Hủy lời mời kết bạn |
44
+ | ✓ | Chấp nhận / Từ chối lời mời kết bạn |
45
+ | ✓ | Hủy kết bạn (xóa bạn) |
46
+ | ✓ | Lấy danh sách bạn bè & lời mời đã gửi |
47
+ | ✓✓ | Xóa tin nhắn của User/Group |
48
+ | ✓✓ | Tắt mở thông báo User/Group |
49
+ | ✓✓ | Cập nhật quyền riêng |
50
+ | ✓ | Cập nhật thông tin nhân (name, gender, avatar..) |
51
+ | | Tìm người dùng bằng SĐT hoặc User ID |
52
+ | ✓ | Lấy thông tin chi tiết người dùng |
53
+ | ✓ | Chặn / Bỏ chặn người dùng |
54
+ | 👥 | **_QUẢN NHÓM_** |
55
+ | ✓ | Tạo nhóm |
56
+ | ✓✓ | Cập nhật cài đặt nhóm |
57
+ | ✓ | Thêm / Xóa thành viên khỏi nhóm |
58
+ | | Giải tán nhóm |
59
+ | ✓ | Thay đổi tên & ảnh đại diện nhóm |
60
+ | ✓ | Bổ nhiệm quyền Phó nhóm |
61
+ | ✓✓ | Chuyển quyền Trưởng nhóm |
62
+ | ✓ | Lấy danh sách tất cả các nhóm đã tham gia |
63
+ | | Lấy thông tin nhóm (từ ID hoặc link) |
64
+ | ✓ | Tham gia nhóm bằng link / Rời nhóm |
65
+ | ✓ | Tạo ghi chú (Note) trong nhóm |
66
+ | 🎨 | **_KHÁC_** |
67
+ | ✓ | Tạo bình chọn (Poll) trong nhóm |
68
+ | ✓ | Quản lý thẻ phân loại (Tag) |
69
+ | ✓ | Tìm kiếm sticker |
67
70
 
68
71
 
69
72
  ## 🚀 Hướng Dẫn Cài Đặt
@@ -159,7 +162,7 @@ Khi bạn quản lý nhiều tài khoản Zalo, mỗi tài khoản sẽ có mộ
159
162
  <details>
160
163
  <summary><b>nhận sự kiện từ Zalo để phản hồi</b></summary>
161
164
 
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.
165
+ - **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.
163
166
  - **Cấu hình**:
164
167
  - Chấp nhận hoặc loại trừ các ID nhóm khi nhận sự kiện tin nhắn.
165
168
  - Chỉ nhận theo từ khoá hoặc loại trừ khi nhận sự kiện tin nhắn.
@@ -200,6 +203,7 @@ Khi bạn quản lý nhiều tài khoản Zalo, mỗi tài khoản sẽ có mộ
200
203
  - **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.
201
204
  - **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.
202
205
  - **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).
206
+ - **Lấy tin nhắn cũ:** Lấy danh sách các tin nhắn cũ trong khả năng.
203
207
  </details>
204
208
 
205
209
  ---
@@ -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': {
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZaloGroup = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
- const ZaloGroupDescription_1 = require("./ZaloGroupDescription"); // eslint-disable-line n8n-nodes-base/node-class-description-icon-not-svg
5
+ const zca_js_1 = require("zca-js");
6
+ const ZaloGroupDescription_1 = require("./ZaloGroupDescription");
6
7
  const ZaloNodeProperties_1 = require("../shared/ZaloNodeProperties");
7
8
  const helper_1 = require("../utils/helper");
8
9
  const zalo_helper_1 = require("../utils/zalo.helper");
@@ -53,16 +54,21 @@ class ZaloGroup {
53
54
  } catch (e) { return false; }
54
55
  });
55
56
 
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.');
57
+ try {
58
+ api = await (0, zalo_helper_1.getZaloApiClient)(this, {needsImageMetadataGetter: needsImageMetadataGetter});
59
+ if (!api) {
60
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, 'Failed to initialize Zalo API. Check credentials or User ID.');
61
+ }
62
+ }
63
+ catch (error) {
64
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
59
65
  }
60
66
 
61
67
  for (let i = 0; i < items.length; i++) {
62
68
  try {
63
69
  const resource = this.getNodeParameter('resource', i);
64
70
  const operation = this.getNodeParameter('operation', i);
65
- this.logger.info(`[GROUP] ${operation} - Image ${needsImageMetadataGetter}`)
71
+ // this.logger.info(`[GROUP] ${operation} - Image ${needsImageMetadataGetter}`)
66
72
 
67
73
  if (resource === 'zaloGroup') {
68
74
  switch (operation) {
@@ -98,89 +104,6 @@ class ZaloGroup {
98
104
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
99
105
  break;
100
106
  }
101
- case 'addUserToGroup': {
102
- const groupId = this.getNodeParameter('groupId', i);
103
- const userIdsParam = this.getNodeParameter('userIds', i, '');
104
- const phoneNumber = this.getNodeParameter('phoneNumber', i, '');
105
- if (!userIdsParam && !phoneNumber) {
106
- returnData.push({
107
- json: {
108
- success: false,
109
- error: 'You must provide either User IDs or a Phone Number.'
110
- },
111
- pairedItem: {
112
- item: i
113
- }
114
- });
115
- }
116
- else {
117
- const userList = new Set();
118
- if (userIdsParam) {
119
- userIdsParam.split(',').map(id => id.trim()).filter(id => id).forEach(id => userList.add(id));
120
- }
121
- if (phoneNumber) {
122
- try {
123
- const userFound = await api.findUser(phoneNumber);
124
- if (userFound && userFound.uid) {
125
- userList.add(userFound.uid);
126
- }
127
- else {
128
- this.logger.warn(`User not found for phone number: ${phoneNumber}. Skipping.`);
129
- }
130
- }
131
- catch (error) {
132
- this.logger.warn(`Failed to find user by phone number ${phoneNumber}: ${error.message}`);
133
- }
134
- }
135
- const finalUserList = Array.from(userList);
136
- if (finalUserList.length > 0) {
137
- const response = await api.addUserToGroup(finalUserList, groupId);
138
- const errorMembers = response.errorMembers || [];
139
- const allErrorMembers = errorMembers.length === finalUserList.length;
140
- returnData.push({
141
- json: { success: true, all_error: allErrorMembers, response: response },
142
- pairedItem: { item: i }
143
- });
144
- }
145
- else {
146
- returnData.push({
147
- json: { success: false, error: 'No valid users to add to the group.' },
148
- pairedItem: { item: i }
149
- });
150
- }
151
- }
152
- break;
153
- }
154
- case 'changeGroupAvatar': {
155
- const groupId = this.getNodeParameter('groupId', i);
156
- const imageUrl = this.getNodeParameter('imageUrl', i);
157
- let finalAvatarSource;
158
- let tempFilePath;
159
- try {
160
- if (!imageUrl) {
161
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Image URL is required for changing group avatar.', { itemIndex: i });
162
- }
163
- try {
164
- new URL(imageUrl);
165
- this.logger.info(`Downloading image from URL for avatar: ${imageUrl}`);
166
- tempFilePath = await (0, helper_1.saveFile)(imageUrl);
167
- finalAvatarSource = tempFilePath;
168
- }
169
- catch (urlError) {
170
- this.logger.info(`Treating image URL as local file path for avatar: ${imageUrl}`);
171
- finalAvatarSource = imageUrl;
172
- }
173
- const response = await api.changeGroupAvatar(finalAvatarSource, groupId);
174
- returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
175
- }
176
- finally {
177
- if (tempFilePath) {
178
- await (0, helper_1.removeFile)(tempFilePath);
179
- this.logger.info(`Successfully removed temporary avatar file: ${tempFilePath}`);
180
- }
181
- }
182
- break;
183
- }
184
107
  case 'changeGroupName': {
185
108
  const groupId = this.getNodeParameter('groupId', i);
186
109
  const newName = this.getNodeParameter('newName', i);
@@ -188,43 +111,6 @@ class ZaloGroup {
188
111
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
189
112
  break;
190
113
  }
191
- case 'getGroupMembers': {
192
- const groupId = this.getNodeParameter('groupId', i);
193
- const response = await api.getGroupInfo(groupId);
194
- const groupDetails = response?.gridInfoMap?.[groupId];
195
- const memberIds = groupDetails?.memVerList || [];
196
- if (memberIds.length > 0) {
197
- const membersInfo = await api.getGroupMembersInfo(memberIds);
198
- const profiles = membersInfo.profiles;
199
- returnData.push({
200
- json: {
201
- success: true,
202
- memberIds,
203
- profiles,
204
- totalMembers: memberIds.length,
205
- groupInfo: groupDetails
206
- },
207
- pairedItem: {
208
- item: i,
209
- },
210
- });
211
- }
212
- else {
213
- returnData.push({
214
- json: {
215
- success: true,
216
- memberIds: [],
217
- profiles: {},
218
- totalMembers: 0,
219
- groupInfo: groupDetails || null
220
- },
221
- pairedItem: {
222
- item: i,
223
- },
224
- });
225
- }
226
- break;
227
- }
228
114
  case 'getAllGroups': {
229
115
  const response = await api.getAllGroups();
230
116
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
@@ -288,21 +174,131 @@ class ZaloGroup {
288
174
  returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
289
175
  break;
290
176
  }
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
- }
177
+ case 'updateGroupSettings' : {
178
+ const groupId = this.getNodeParameter('groupId', i);
179
+ const settings = this.getNodeParameter('settings', i, {});
180
+ const response = await api.updateGroupSettings(settings, groupId);
181
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
182
+ break;
183
+ }
184
+ case 'getGroupMembers': {
185
+ const groupId = this.getNodeParameter('groupId', i);
186
+ const response = await api.getGroupInfo(groupId);
187
+ const groupDetails = response?.gridInfoMap?.[groupId];
188
+ const memberIds = groupDetails?.memVerList || [];
189
+ if (memberIds.length > 0) {
190
+ const membersInfo = await api.getGroupMembersInfo(memberIds);
191
+ const profiles = membersInfo.profiles;
192
+ returnData.push({
193
+ json: {
194
+ success: true,
195
+ memberIds,
196
+ profiles,
197
+ totalMembers: memberIds.length,
198
+ groupInfo: groupDetails
199
+ },
200
+ pairedItem: {
201
+ item: i,
202
+ },
203
+ });
204
+ }
205
+ else {
206
+ returnData.push({
207
+ json: {
208
+ success: true,
209
+ memberIds: [],
210
+ profiles: {},
211
+ totalMembers: 0,
212
+ groupInfo: groupDetails || null
213
+ },
214
+ pairedItem: {
215
+ item: i,
216
+ },
217
+ });
218
+ }
219
+ break;
220
+ }
221
+ case 'addUserToGroup': {
222
+ const groupId = this.getNodeParameter('groupId', i);
223
+ const userIdsParam = this.getNodeParameter('userIds', i, '');
224
+ const phoneNumber = this.getNodeParameter('phoneNumber', i, '');
225
+ if (!userIdsParam && !phoneNumber) {
226
+ returnData.push({
227
+ json: {
228
+ success: false,
229
+ error: 'You must provide either User IDs or a Phone Number.'
230
+ },
231
+ pairedItem: {
232
+ item: i
233
+ }
234
+ });
235
+ }
236
+ else {
237
+ const userList = new Set();
238
+ if (userIdsParam) {
239
+ userIdsParam.split(',').map(id => id.trim()).filter(id => id).forEach(id => userList.add(id));
240
+ }
241
+ if (phoneNumber) {
242
+ try {
243
+ const userFound = await api.findUser(phoneNumber);
244
+ if (userFound && userFound.uid) {
245
+ userList.add(userFound.uid);
246
+ }
247
+ else {
248
+ this.logger.warn(`User not found for phone number: ${phoneNumber}. Skipping.`);
249
+ }
250
+ }
251
+ catch (error) {
252
+ this.logger.warn(`Failed to find user by phone number ${phoneNumber}: ${error.message}`);
253
+ }
254
+ }
255
+ const finalUserList = Array.from(userList);
256
+ if (finalUserList.length > 0) {
257
+ const response = await api.addUserToGroup(finalUserList, groupId);
258
+ const errorMembers = response.errorMembers || [];
259
+ const allErrorMembers = errorMembers.length === finalUserList.length;
260
+ returnData.push({
261
+ json: { success: true, all_error: allErrorMembers, response: response },
262
+ pairedItem: { item: i }
263
+ });
264
+ }
265
+ else {
266
+ returnData.push({
267
+ json: { success: false, error: 'No valid users to add to the group.' },
268
+ pairedItem: { item: i }
269
+ });
270
+ }
271
+ }
272
+ break;
273
+ }
274
+ case 'changeGroupAvatar': {
275
+ const groupId = this.getNodeParameter('groupId', i);
276
+ const imageUrl = this.getNodeParameter('imageUrl', i);
277
+ let finalAvatarSource;
278
+ let tempFilePath;
279
+ try {
280
+ if (!imageUrl) {
281
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Image URL is required for changing group avatar.', { itemIndex: i });
282
+ }
283
+ try {
284
+ new URL(imageUrl);
285
+ tempFilePath = await (0, helper_1.saveFile)(imageUrl);
286
+ finalAvatarSource = tempFilePath;
287
+ }
288
+ catch (urlError) {
289
+ finalAvatarSource = imageUrl;
290
+ }
291
+ const response = await api.changeGroupAvatar(finalAvatarSource, groupId);
292
+ returnData.push({ json: { success: true, response }, pairedItem: { item: i } });
293
+ }
294
+ finally {
295
+ if (tempFilePath) {
296
+ await (0, helper_1.removeFile)(tempFilePath);
297
+ this.logger.info(`Successfully removed temporary avatar file: ${tempFilePath}`);
298
+ }
299
+ }
300
+ break;
301
+ }
306
302
  }
307
303
  }
308
304
  }