n8n-nodes-zalo-custom 1.0.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/LICENSE.md +19 -0
- package/README.md +235 -0
- package/credentials/N8nZaloApi.credentials.js +33 -0
- package/credentials/ZaloApi.credentials.js +107 -0
- package/credentials/shared/n8n.png +0 -0
- package/credentials/shared/zalo.svg +10 -0
- package/index.js +0 -0
- package/nodes/ZaloCommunication/ZaloCommunication.node.js +420 -0
- package/nodes/ZaloCommunication/ZaloCommunicationDescription.js +524 -0
- package/nodes/ZaloGroup/ZaloGroup.node.js +309 -0
- package/nodes/ZaloGroup/ZaloGroupDescription.js +318 -0
- package/nodes/ZaloLoginByQr/ZaloLoginByQr.node.js +579 -0
- package/nodes/ZaloSendMessage/ZaloSendMessage.node.js +773 -0
- package/nodes/ZaloTrigger/ZaloTrigger.node.js +609 -0
- package/nodes/ZaloUploadAttachment/ZaloUploadAttachment.node.js +265 -0
- package/nodes/ZaloUploadAttachment/ZaloUploadAttachmentDescription.js +187 -0
- package/nodes/ZaloUser/ZaloUser.node.js +212 -0
- package/nodes/ZaloUser/ZaloUserDescription.js +361 -0
- package/nodes/shared/ZaloNodeProperties.js +55 -0
- package/nodes/shared/zalo.svg +10 -0
- package/nodes/utils/crypto.helper.js +57 -0
- package/nodes/utils/helper.js +41 -0
- package/nodes/utils/zalo.helper.js +324 -0
- package/package.json +60 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZaloSendMessage = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const zca_js_1 = require("zca-js");
|
|
6
|
+
const helper_1 = require("../utils/helper");
|
|
7
|
+
const zalo_helper_1 = require("../utils/zalo.helper");
|
|
8
|
+
const ZaloNodeProperties_1 = require("../shared/ZaloNodeProperties");
|
|
9
|
+
const reactionOptions = [
|
|
10
|
+
{ name: 'Heart', value: 'heart' },
|
|
11
|
+
{ name: 'Like', value: 'like' },
|
|
12
|
+
{ name: 'Haha', value: 'haha' },
|
|
13
|
+
{ name: 'Wow', value: 'wow' },
|
|
14
|
+
{ name: 'Cry', value: 'cry' },
|
|
15
|
+
{ name: 'Angry', value: 'angry' },
|
|
16
|
+
{ name: 'Kiss', value: 'kiss' },
|
|
17
|
+
{ name: 'Tears of Joy', value: 'tears_of_joy' },
|
|
18
|
+
{ name: 'Dislike', value: 'dislike' },
|
|
19
|
+
{ name: 'OK', value: 'ok' },
|
|
20
|
+
{ name: 'Thanks', value: 'thanks' },
|
|
21
|
+
{ name: 'Big Smile', value: 'big_smile' },
|
|
22
|
+
{ name: 'Sad', value: 'sad' },
|
|
23
|
+
{ name: 'Very Sad', value: 'very_sad' },
|
|
24
|
+
{ name: 'Rose', value: 'rose' },
|
|
25
|
+
{ name: 'Broken Heart', value: 'broken_heart' },
|
|
26
|
+
{ name: 'Love', value: 'love' },
|
|
27
|
+
{ name: 'Confused', value: 'confused' },
|
|
28
|
+
{ name: 'Wink', value: 'wink' },
|
|
29
|
+
{ name: 'Birthday', value: 'birthday' },
|
|
30
|
+
{ name: 'Bomb', value: 'bomb' },
|
|
31
|
+
{ name: 'Peace', value: 'peace' },
|
|
32
|
+
{ name: 'Pray', value: 'pray' },
|
|
33
|
+
{ name: 'No', value: 'no' },
|
|
34
|
+
{ name: 'Love You', value: 'love_you' },
|
|
35
|
+
{ name: 'Cool', value: 'cool' },
|
|
36
|
+
{ name: 'Nerd', value: 'nerd' },
|
|
37
|
+
{ name: 'Sunglasses', value: 'sunglasses' },
|
|
38
|
+
{ name: 'Neutral', value: 'neutral' },
|
|
39
|
+
{ name: 'Bye', value: 'bye' },
|
|
40
|
+
{ name: 'Sleepy', value: 'sleepy' },
|
|
41
|
+
{ name: 'Wipe', value: 'wipe' },
|
|
42
|
+
{ name: 'Dig', value: 'dig' },
|
|
43
|
+
{ name: 'Handclap', value: 'handclap' },
|
|
44
|
+
{ name: 'Silent', value: 'silent' },
|
|
45
|
+
{ name: 'Surprise', value: 'surprise' },
|
|
46
|
+
{ name: 'Embarrassed', value: 'embarrassed' },
|
|
47
|
+
{ name: 'Afraid', value: 'afraid' },
|
|
48
|
+
{ name: 'Big Laugh', value: 'big_laugh' },
|
|
49
|
+
{ name: 'Beer', value: 'beer' },
|
|
50
|
+
];
|
|
51
|
+
let api;
|
|
52
|
+
function parseHtmlToStyles(html) {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
let workingHtml = html
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/'/g, "'")
|
|
57
|
+
.replace(/</g, '<')
|
|
58
|
+
.replace(/>/g, '>')
|
|
59
|
+
.replace(/&/g, '&')
|
|
60
|
+
.replace(/ /g, ' ');
|
|
61
|
+
workingHtml = workingHtml.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2');
|
|
62
|
+
workingHtml = workingHtml.replace(/<br\s*\/?>/gi, '___BR___');
|
|
63
|
+
workingHtml = workingHtml.replace(/>\s+</g, '><');
|
|
64
|
+
workingHtml = workingHtml.replace(/___BR___/g, '<br>');
|
|
65
|
+
const styles = [];
|
|
66
|
+
const tagMappings = {
|
|
67
|
+
'b': 'b',
|
|
68
|
+
'strong': 'b',
|
|
69
|
+
'i': 'i',
|
|
70
|
+
'em': 'i',
|
|
71
|
+
'u': 'u',
|
|
72
|
+
's': 's',
|
|
73
|
+
'strike': 's',
|
|
74
|
+
'del': 's',
|
|
75
|
+
'small': 'f_13',
|
|
76
|
+
'big': 'f_18',
|
|
77
|
+
'red': 'c_db342e',
|
|
78
|
+
'orange': 'c_f27806',
|
|
79
|
+
'yellow': 'c_f7b503',
|
|
80
|
+
'green': 'c_15a85f',
|
|
81
|
+
};
|
|
82
|
+
const openTags = [];
|
|
83
|
+
let cleanText = '';
|
|
84
|
+
let htmlIndex = 0;
|
|
85
|
+
let textIndex = 0;
|
|
86
|
+
while (htmlIndex < workingHtml.length) {
|
|
87
|
+
const char = workingHtml[htmlIndex];
|
|
88
|
+
if (char === '<') {
|
|
89
|
+
const tagEndIndex = workingHtml.indexOf('>', htmlIndex);
|
|
90
|
+
if (tagEndIndex === -1) {
|
|
91
|
+
cleanText += char;
|
|
92
|
+
textIndex++;
|
|
93
|
+
htmlIndex++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const tagContent = workingHtml.substring(htmlIndex + 1, tagEndIndex);
|
|
97
|
+
const isClosingTag = tagContent.startsWith('/');
|
|
98
|
+
const tagName = (isClosingTag ? tagContent.substring(1) : tagContent.split(/\s/)[0]).toLowerCase();
|
|
99
|
+
if (tagName === 'br') {
|
|
100
|
+
cleanText += '\n';
|
|
101
|
+
textIndex++;
|
|
102
|
+
}
|
|
103
|
+
else if (tagName === 'p') {
|
|
104
|
+
if (isClosingTag) {
|
|
105
|
+
cleanText += '\n\n';
|
|
106
|
+
textIndex += 2;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (tagName === 'div') {
|
|
110
|
+
if (isClosingTag) {
|
|
111
|
+
cleanText += '\n';
|
|
112
|
+
textIndex++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (tagName.match(/^h[1-6]$/)) {
|
|
116
|
+
if (isClosingTag) {
|
|
117
|
+
cleanText += '\n';
|
|
118
|
+
textIndex++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (tagName === 'li') {
|
|
122
|
+
if (!isClosingTag) {
|
|
123
|
+
cleanText += '• ';
|
|
124
|
+
textIndex += 2;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
cleanText += '\n';
|
|
128
|
+
textIndex++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (tagName === 'ul' || tagName === 'ol') {
|
|
132
|
+
if (!isClosingTag) {
|
|
133
|
+
cleanText += '\n';
|
|
134
|
+
textIndex++;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
cleanText += '\n';
|
|
138
|
+
textIndex++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const styleType = tagMappings[tagName];
|
|
143
|
+
if (styleType) {
|
|
144
|
+
if (isClosingTag) {
|
|
145
|
+
for (let i = openTags.length - 1; i >= 0; i--) {
|
|
146
|
+
if (openTags[i].tagName === tagName) {
|
|
147
|
+
const openTag = openTags[i];
|
|
148
|
+
if (textIndex > openTag.startPos) {
|
|
149
|
+
styles.push({
|
|
150
|
+
st: openTag.styleType,
|
|
151
|
+
start: openTag.startPos,
|
|
152
|
+
end: textIndex
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
openTags.splice(i, 1);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
openTags.push({
|
|
162
|
+
tagName: tagName,
|
|
163
|
+
styleType: styleType,
|
|
164
|
+
startPos: textIndex
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
htmlIndex = tagEndIndex + 1;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
cleanText += char;
|
|
173
|
+
textIndex++;
|
|
174
|
+
htmlIndex++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const openTag of openTags) {
|
|
178
|
+
if (textIndex > openTag.startPos) {
|
|
179
|
+
styles.push({
|
|
180
|
+
st: openTag.styleType,
|
|
181
|
+
start: openTag.startPos,
|
|
182
|
+
end: textIndex
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
cleanText = cleanText
|
|
187
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
188
|
+
.replace(/[ \t]+/g, ' ');
|
|
189
|
+
const trimStart = ((_b = (_a = cleanText.match(/^\s*/)) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.length) || 0;
|
|
190
|
+
cleanText = cleanText.replace(/^\s+|\s+$/g, '');
|
|
191
|
+
if (trimStart > 0) {
|
|
192
|
+
for (const style of styles) {
|
|
193
|
+
style.start = Math.max(0, style.start - trimStart);
|
|
194
|
+
style.end = Math.max(style.start, style.end - trimStart);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { cleanText, styles };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class ZaloSendMessage {
|
|
201
|
+
constructor() {
|
|
202
|
+
this.description = {
|
|
203
|
+
displayName: 'Zalo Send Message',
|
|
204
|
+
name: 'zaloSendMessage',
|
|
205
|
+
icon: 'file:../shared/zalo.svg',
|
|
206
|
+
group: ['Zalo'],
|
|
207
|
+
version: 4,
|
|
208
|
+
documentationUrl: 'https://www.npmjs.com/package/n8n-nodes-zalo/',
|
|
209
|
+
description: 'Gửi tin nhắn qua API Zalo Credential',
|
|
210
|
+
defaults: {
|
|
211
|
+
name: 'Zalo Send Message',
|
|
212
|
+
},
|
|
213
|
+
inputs: ['main'],
|
|
214
|
+
outputs: ['main'],
|
|
215
|
+
credentials: [...ZaloNodeProperties_1.zaloApiCredential],
|
|
216
|
+
properties: [
|
|
217
|
+
...ZaloNodeProperties_1.zaloSessionProperties,
|
|
218
|
+
{
|
|
219
|
+
displayName: 'Thread ID',
|
|
220
|
+
name: 'threadId',
|
|
221
|
+
type: 'string',
|
|
222
|
+
default: '',
|
|
223
|
+
required: true,
|
|
224
|
+
description: 'ID của thread để gửi tin nhắn',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
displayName: 'Type',
|
|
228
|
+
name: 'type',
|
|
229
|
+
type: 'options',
|
|
230
|
+
options: [
|
|
231
|
+
{
|
|
232
|
+
name: 'User',
|
|
233
|
+
value: 0,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'Group',
|
|
237
|
+
value: 1,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
default: 0,
|
|
241
|
+
description: 'Tin nhắn gửi đến (người dùng hoặc nhóm)',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
displayName: 'Message',
|
|
245
|
+
name: 'message',
|
|
246
|
+
type: 'string',
|
|
247
|
+
default: '',
|
|
248
|
+
required: true,
|
|
249
|
+
description: 'Nội dung tin nhắn cần gửi',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
displayName: 'Message Format',
|
|
253
|
+
name: 'messageFormat',
|
|
254
|
+
type: 'options',
|
|
255
|
+
options: [
|
|
256
|
+
{
|
|
257
|
+
name: 'Plain Text',
|
|
258
|
+
value: 'plain',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'HTML (Auto Convert)',
|
|
262
|
+
value: 'html',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
default: 'plain',
|
|
266
|
+
description: 'Định dạng tin nhắn: Plain text hoặc HTML tự động chuyển đổi',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
displayName: 'Urgency',
|
|
270
|
+
name: 'urgency',
|
|
271
|
+
type: 'options',
|
|
272
|
+
options: [
|
|
273
|
+
{
|
|
274
|
+
name: 'Default',
|
|
275
|
+
value: 0,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'Important',
|
|
279
|
+
value: 1,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'Urgent',
|
|
283
|
+
value: 2,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
default: 0,
|
|
287
|
+
description: 'Mức độ khẩn cấp của tin nhắn',
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
displayName: 'Image URLs',
|
|
291
|
+
name: 'attachmentUrls',
|
|
292
|
+
type: 'string',
|
|
293
|
+
default: '',
|
|
294
|
+
placeholder: 'https://../img1.jpg, https://../img2.png',
|
|
295
|
+
description: 'Nhập 1 hoặc nhiều url ảnh cách nhau với dấu (,)',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
displayName: 'Send "Typing..." Event',
|
|
299
|
+
name: 'sendTypingEvent',
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
default: true,
|
|
302
|
+
description: 'Gửi sự kiện "Đang soạn tin..." và đợi một chút trước khi gửi tin nhắn để mô phỏng hành vi của người dùng',
|
|
303
|
+
},
|
|
304
|
+
// reaction
|
|
305
|
+
{
|
|
306
|
+
displayName: 'Reaction',
|
|
307
|
+
name: 'reaction',
|
|
308
|
+
type: 'fixedCollection',
|
|
309
|
+
typeOptions: {
|
|
310
|
+
multipleValues: false,
|
|
311
|
+
},
|
|
312
|
+
placeholder: 'Add Reaction',
|
|
313
|
+
default: {},
|
|
314
|
+
options: [
|
|
315
|
+
{
|
|
316
|
+
displayName: 'Reaction Settings',
|
|
317
|
+
name: 'reactionValue',
|
|
318
|
+
values: [
|
|
319
|
+
{
|
|
320
|
+
displayName: 'Reaction Icon',
|
|
321
|
+
name: 'reactionIcon',
|
|
322
|
+
type: 'options',
|
|
323
|
+
options: reactionOptions,
|
|
324
|
+
default: 'heart',
|
|
325
|
+
description: 'Biểu tượng cảm xúc để thể hiện',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
displayName: 'Message ID',
|
|
329
|
+
name: 'reactionMsgId',
|
|
330
|
+
type: 'string',
|
|
331
|
+
default: '',
|
|
332
|
+
description: 'msgId của tin nhắn hiện có để reaction',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
displayName: 'Client Message ID',
|
|
336
|
+
name: 'reactionCliMsgId',
|
|
337
|
+
type: 'string',
|
|
338
|
+
default: '',
|
|
339
|
+
description: 'cliMsgId của tin nhắn hiện có để reaction',
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
{
|
|
347
|
+
displayName: 'Quote Message',
|
|
348
|
+
name: 'quote',
|
|
349
|
+
type: 'fixedCollection',
|
|
350
|
+
typeOptions: {
|
|
351
|
+
multipleValues: false,
|
|
352
|
+
},
|
|
353
|
+
placeholder: 'Add Quote',
|
|
354
|
+
default: {},
|
|
355
|
+
options: [
|
|
356
|
+
{
|
|
357
|
+
displayName: 'Quote Details',
|
|
358
|
+
name: 'quoteValue',
|
|
359
|
+
values: [
|
|
360
|
+
{
|
|
361
|
+
displayName: 'User ID',
|
|
362
|
+
name: 'uidFrom',
|
|
363
|
+
type: 'string',
|
|
364
|
+
default: '',
|
|
365
|
+
description: 'ID của người gửi tin nhắn (không phải threadId của group)',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
displayName: 'Message ID',
|
|
369
|
+
name: 'msgId',
|
|
370
|
+
type: 'string',
|
|
371
|
+
default: '',
|
|
372
|
+
description: 'msgId của tin nhắn cần trích dẫn',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
displayName: 'Client Message ID',
|
|
376
|
+
name: 'cliMsgId',
|
|
377
|
+
type: 'string',
|
|
378
|
+
default: '',
|
|
379
|
+
description: 'cliMsgId của tin nhắn cần trích dẫn',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
displayName: 'Content',
|
|
383
|
+
name: 'content',
|
|
384
|
+
type: 'string',
|
|
385
|
+
default: '',
|
|
386
|
+
description: 'Nội dung tin nhắn trích dẫn',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
displayName: 'Timestamp',
|
|
390
|
+
name: 'ts',
|
|
391
|
+
type: 'number',
|
|
392
|
+
default: 0,
|
|
393
|
+
description: 'Timestamp (ms) của tin nhắn cần trích dẫn',
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
displayName: 'Message Type',
|
|
397
|
+
name: 'msgType',
|
|
398
|
+
type: 'number',
|
|
399
|
+
default: 1,
|
|
400
|
+
description: 'Loại tin nhắn được trích dẫn (ví dụ: 1 cho text)',
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
displayName: 'Mentions',
|
|
408
|
+
name: 'mentions',
|
|
409
|
+
type: 'fixedCollection',
|
|
410
|
+
typeOptions: {
|
|
411
|
+
multipleValues: false,
|
|
412
|
+
},
|
|
413
|
+
placeholder: 'Add Mention',
|
|
414
|
+
default: {},
|
|
415
|
+
options: [
|
|
416
|
+
{
|
|
417
|
+
displayName: 'Mention Details',
|
|
418
|
+
name: 'mentionValue',
|
|
419
|
+
values: [
|
|
420
|
+
{
|
|
421
|
+
displayName: 'User ID',
|
|
422
|
+
name: 'uid',
|
|
423
|
+
type: 'string',
|
|
424
|
+
default: '',
|
|
425
|
+
description: 'ID của người dùng được mention',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
displayName: 'Position',
|
|
429
|
+
name: 'pos',
|
|
430
|
+
type: 'number',
|
|
431
|
+
default: 0,
|
|
432
|
+
description: 'Vị trí mention trong tin nhắn',
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
displayName: 'Length',
|
|
436
|
+
name: 'len',
|
|
437
|
+
type: 'number',
|
|
438
|
+
default: 0,
|
|
439
|
+
description: 'Độ dài của mention',
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
displayName: 'Text Styles',
|
|
447
|
+
name: 'styles',
|
|
448
|
+
type: 'fixedCollection',
|
|
449
|
+
typeOptions: {
|
|
450
|
+
multipleValues: true,
|
|
451
|
+
},
|
|
452
|
+
placeholder: 'Add Text Style',
|
|
453
|
+
default: {},
|
|
454
|
+
options: [
|
|
455
|
+
{
|
|
456
|
+
name: 'style',
|
|
457
|
+
displayName: 'Style',
|
|
458
|
+
values: [
|
|
459
|
+
{
|
|
460
|
+
displayName: 'Style Type',
|
|
461
|
+
name: 'st',
|
|
462
|
+
type: 'options',
|
|
463
|
+
options: [
|
|
464
|
+
{
|
|
465
|
+
name: 'Big Font',
|
|
466
|
+
value: 'f_18',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'Bold',
|
|
470
|
+
value: 'b',
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'Green Color',
|
|
474
|
+
value: 'c_15a85f',
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'Indent',
|
|
478
|
+
value: 'ind_$',
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: 'Italic',
|
|
482
|
+
value: 'i',
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'Orange Color',
|
|
486
|
+
value: 'c_f27806',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'Ordered List',
|
|
490
|
+
value: 'lst_2',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'Red Color',
|
|
494
|
+
value: 'c_db342e',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: 'Small Font',
|
|
498
|
+
value: 'f_13',
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'Strike Through',
|
|
502
|
+
value: 's',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'Underline',
|
|
506
|
+
value: 'u',
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: 'Unordered List',
|
|
510
|
+
value: 'lst_1',
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: 'Yellow Color',
|
|
514
|
+
value: 'c_f7b503',
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
default: 'b',
|
|
518
|
+
description: 'Loại style áp dụng cho text',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
displayName: 'Start Position',
|
|
522
|
+
name: 'start',
|
|
523
|
+
type: 'number',
|
|
524
|
+
default: 0,
|
|
525
|
+
description: 'Vị trí bắt đầu áp dụng style (tính từ 0)',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
displayName: 'End Position',
|
|
529
|
+
name: 'end',
|
|
530
|
+
type: 'number',
|
|
531
|
+
default: 0,
|
|
532
|
+
description: 'Vị trí kết thúc áp dụng style',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
displayName: 'Indent Size',
|
|
536
|
+
name: 'indentSize',
|
|
537
|
+
type: 'number',
|
|
538
|
+
default: 1,
|
|
539
|
+
displayOptions: {
|
|
540
|
+
show: {
|
|
541
|
+
'st': ['ind_$'],
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
description: 'Kích thước indent (chỉ dùng cho Indent style)',
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
description: 'Định dạng text với các style khác nhau',
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
displayName: 'TTL (Time To Live)',
|
|
553
|
+
name: 'ttl',
|
|
554
|
+
type: 'number',
|
|
555
|
+
default: 0,
|
|
556
|
+
description: 'Thời gian tồn tại của tin nhắn (giây). 0 = vĩnh viễn.',
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
async execute() {
|
|
562
|
+
const returnData = [];
|
|
563
|
+
const items = this.getInputData();
|
|
564
|
+
try {
|
|
565
|
+
// Use the new helper function to get the API client
|
|
566
|
+
api = await (0, zalo_helper_1.getZaloApiClient)(this, { needsImageMetadataGetter: true });
|
|
567
|
+
if (!api) {
|
|
568
|
+
// The helper function will throw an error, but as a fallback:
|
|
569
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to initialize Zalo API. Check credentials or User ID.');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
this.logger.error(`Zalo login error: ${error.message}`);
|
|
574
|
+
returnData.push({
|
|
575
|
+
json: {
|
|
576
|
+
success: false,
|
|
577
|
+
error: `Zalo login error: ${error.message}`
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
return [returnData]
|
|
581
|
+
// throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Zalo login error: ${error.message}`);
|
|
582
|
+
}
|
|
583
|
+
for (let i = 0; i < items.length; i++) {
|
|
584
|
+
try {
|
|
585
|
+
const threadId = this.getNodeParameter('threadId', i);
|
|
586
|
+
const typeNumber = this.getNodeParameter('type', i);
|
|
587
|
+
const type = typeNumber === 0 ? zca_js_1.ThreadType.User : zca_js_1.ThreadType.Group;
|
|
588
|
+
const rawMessage = this.getNodeParameter('message', i);
|
|
589
|
+
const messageFormat = this.getNodeParameter('messageFormat', i, 'plain');
|
|
590
|
+
const sendTypingEvent = this.getNodeParameter('sendTypingEvent', i, false);
|
|
591
|
+
const urgency = this.getNodeParameter('urgency', i, 0);
|
|
592
|
+
const quoteParam = this.getNodeParameter('quote', i, {});
|
|
593
|
+
const mentionsParam = this.getNodeParameter('mentions', i, {});
|
|
594
|
+
const manualStyles = this.getNodeParameter('styles', i, {});
|
|
595
|
+
const ttl = this.getNodeParameter('ttl', i, 0);
|
|
596
|
+
const attachmentUrls = this.getNodeParameter('attachmentUrls', i, '');
|
|
597
|
+
const reactionParam = this.getNodeParameter('reaction', i, {});
|
|
598
|
+
|
|
599
|
+
// return json
|
|
600
|
+
const returnJson = {
|
|
601
|
+
threadId,
|
|
602
|
+
threadType: type
|
|
603
|
+
};
|
|
604
|
+
let reactionIcon;
|
|
605
|
+
let reactionMsgId;
|
|
606
|
+
let reactionCliMsgId;
|
|
607
|
+
if (reactionParam && reactionParam.reactionValue && Object.keys(reactionParam.reactionValue).length > 0) {
|
|
608
|
+
const r = reactionParam.reactionValue;
|
|
609
|
+
const reactionKey = (r.reactionIcon || 'heart').toUpperCase();
|
|
610
|
+
reactionIcon = zca_js_1.Reactions[reactionKey] || zca_js_1.Reactions.HEART;
|
|
611
|
+
reactionMsgId = r.reactionMsgId || '';
|
|
612
|
+
reactionCliMsgId = r.reactionCliMsgId || '';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let message = rawMessage;
|
|
616
|
+
let styles = manualStyles;
|
|
617
|
+
if (messageFormat === 'html') {
|
|
618
|
+
const parsed = parseHtmlToStyles(rawMessage);
|
|
619
|
+
message = parsed.cleanText;
|
|
620
|
+
const htmlStyles = parsed.styles;
|
|
621
|
+
const manualStylesArray = manualStyles && manualStyles.style ? manualStyles.style : [];
|
|
622
|
+
styles = {
|
|
623
|
+
style: [...htmlStyles, ...manualStylesArray]
|
|
624
|
+
};
|
|
625
|
+
this.logger.info(`HTML parsed: "${rawMessage}" -> "${message}" with ${htmlStyles.length} styles`);
|
|
626
|
+
this.logger.info(`Parsed styles: ${JSON.stringify(htmlStyles, null, 2)}`);
|
|
627
|
+
}
|
|
628
|
+
const messageContent = {
|
|
629
|
+
msg: message,
|
|
630
|
+
};
|
|
631
|
+
if (urgency !== 0) {
|
|
632
|
+
messageContent.urgency = urgency;
|
|
633
|
+
}
|
|
634
|
+
if (ttl !== 0) {
|
|
635
|
+
messageContent.ttl = ttl;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (styles && styles.style && styles.style.length > 0) {
|
|
639
|
+
messageContent.styles = styles.style.map((style) => ({
|
|
640
|
+
st: style.st,
|
|
641
|
+
start: style.start,
|
|
642
|
+
len: style.end - style.start,
|
|
643
|
+
indentSize: style.st === 'ind_$' ? style.indentSize : undefined,
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
if (quoteParam && quoteParam.quoteValue && Object.keys(quoteParam.quoteValue).length > 0) {
|
|
647
|
+
const quote = quoteParam.quoteValue;
|
|
648
|
+
if (quote.uidFrom && quote.msgId && quote.cliMsgId && quote.content) {
|
|
649
|
+
if (typeNumber === 1 && quote.uidFrom === threadId) {
|
|
650
|
+
returnJson.quoteError = 'tại Nhóm: cần cung cấp ID người gửi tin nhắn thay vì threadId';
|
|
651
|
+
} else {
|
|
652
|
+
messageContent.quote = {
|
|
653
|
+
uidFrom: quote.uidFrom,
|
|
654
|
+
msgId: quote.msgId,
|
|
655
|
+
cliMsgId: quote.cliMsgId,
|
|
656
|
+
content: quote.content,
|
|
657
|
+
ts: quote.ts || Date.now(),
|
|
658
|
+
msgType: quote.msgType || 1,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
this.logger.warn(`..quote: thiếu thông tin: uidFrom, msgId, cliMsgId, content.`);
|
|
663
|
+
returnJson.quoteError = 'thiếu thông tin trích dẫn';
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (mentionsParam && mentionsParam.mentionValue && Object.keys(mentionsParam.mentionValue).length > 0) {
|
|
667
|
+
const mentions = mentionsParam.mentionValue;
|
|
668
|
+
if (mentions.uid) {
|
|
669
|
+
messageContent.mentions = [{
|
|
670
|
+
pos: mentions.pos || 0,
|
|
671
|
+
uid: mentions.uid,
|
|
672
|
+
len: mentions.len || 0,
|
|
673
|
+
}];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (attachmentUrls && attachmentUrls.trim() !== '') {
|
|
677
|
+
const urls = attachmentUrls.split(',').map(url => url.trim()).filter(url => url);
|
|
678
|
+
messageContent.attachments = [];
|
|
679
|
+
for (const url of urls) {
|
|
680
|
+
try {
|
|
681
|
+
// Validate URL format
|
|
682
|
+
new URL(url);
|
|
683
|
+
// Download file and get its local path
|
|
684
|
+
const fileData = await (0, helper_1.saveFile)(url);
|
|
685
|
+
messageContent.attachments.push(fileData);
|
|
686
|
+
this.logger.info(`Successfully prepared attachment from URL: ${url}`);
|
|
687
|
+
}
|
|
688
|
+
catch (e) {
|
|
689
|
+
// Log a warning and skip if the URL is invalid or the file can't be downloaded
|
|
690
|
+
this.logger.warn(`Skipping invalid or inaccessible attachment URL: ${url}. Error: ${e.message}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
this.logger.info(`Sending message with parameters: ${JSON.stringify(messageContent)}`);
|
|
696
|
+
this.logger.info(`Final styles being sent: ${JSON.stringify(messageContent.styles || 'No styles')}`);
|
|
697
|
+
if (!api) {
|
|
698
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Zalo API not initialized');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (sendTypingEvent) {
|
|
702
|
+
try {
|
|
703
|
+
const result = await api.sendTypingEvent(threadId, type);
|
|
704
|
+
if (result) {
|
|
705
|
+
this.logger.info("Sent 'typing' event, waiting for second...");
|
|
706
|
+
returnJson.typing = true;
|
|
707
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
this.logger.warn("'typing' event was sent but returned a falsy result.");
|
|
711
|
+
returnJson.typing = false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
this.logger.warn(`Could not send 'typing' event: ${e.message}`);
|
|
716
|
+
returnJson.typing = false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const response = await api.sendMessage(messageContent, threadId, type);
|
|
720
|
+
returnJson.success = true;
|
|
721
|
+
returnJson.sendMessageResponse = response;
|
|
722
|
+
returnJson.sendMessageValue = messageContent;
|
|
723
|
+
if (reactionIcon && reactionMsgId && reactionCliMsgId) {
|
|
724
|
+
this.logger.info('Waiting 1 second before sending reaction...');
|
|
725
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
726
|
+
try {
|
|
727
|
+
const addReactionDestination = {
|
|
728
|
+
data: {
|
|
729
|
+
msgId: reactionMsgId,
|
|
730
|
+
cliMsgId: reactionCliMsgId,
|
|
731
|
+
},
|
|
732
|
+
threadId,
|
|
733
|
+
type,
|
|
734
|
+
};
|
|
735
|
+
const reactionResponse = await api.addReaction(reactionIcon, addReactionDestination);
|
|
736
|
+
this.logger.info(`Successfully sent reaction '${reactionIcon}' to message ${reactionMsgId}`);
|
|
737
|
+
returnJson.reactionSuccess = true;
|
|
738
|
+
returnJson.reactionResponse = reactionResponse;
|
|
739
|
+
}
|
|
740
|
+
catch (reactionError) {
|
|
741
|
+
this.logger.warn(`Failed to send reaction: ${reactionError.message}`);
|
|
742
|
+
returnJson.reactionSuccess = false;
|
|
743
|
+
returnJson.reactionError = reactionError.message;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (messageContent.attachments && messageContent.attachments.length > 0) {
|
|
747
|
+
for (const attachment of messageContent.attachments) {
|
|
748
|
+
this.logger.info(`Remove attachment: ${attachment}`);
|
|
749
|
+
(0, helper_1.removeFile)(attachment);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
this.logger.info('Message sent successfully', { threadId, type });
|
|
753
|
+
returnData.push({ json: returnJson });
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
this.logger.error('Error sending Zalo message:', error);
|
|
757
|
+
if (this.continueOnFail()) {
|
|
758
|
+
returnData.push({
|
|
759
|
+
json: {
|
|
760
|
+
success: false,
|
|
761
|
+
error: error.message,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return [returnData];
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
exports.ZaloSendMessage = ZaloSendMessage;
|