@softtynet/zca-js 2.3.5 → 2.3.7
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 +25 -2
- package/dist/apis/business/blockOA.d.ts +2 -0
- package/dist/apis/business/blockOA.js +28 -0
- package/dist/apis/business/convertOAIds.d.ts +4 -0
- package/dist/apis/business/convertOAIds.js +26 -0
- package/dist/apis/business/getOAProfile.d.ts +13 -0
- package/dist/apis/business/getOAProfile.js +64 -0
- package/dist/apis/business/unblockOA.d.ts +2 -0
- package/dist/apis/business/unblockOA.js +28 -0
- package/dist/apis/chat/preloadConversations.d.ts +3 -2
- package/dist/apis/chat/preloadConversations.js +14 -0
- package/dist/apis/sync/downloadTransferSync.js +19 -1
- package/dist/apis/sync/getSyncData.js +1 -1
- package/dist/apis.d.ts +12 -0
- package/dist/apis.js +20 -0
- package/dist/call-engine/ZaloCallTransport.js +28 -2
- package/dist/call-engine/ZaloRtcSession.js +9 -1
- package/dist/cjs/apis/business/blockOA.cjs +33 -0
- package/dist/cjs/apis/business/convertOAIds.cjs +31 -0
- package/dist/cjs/apis/business/getOAProfile.cjs +69 -0
- package/dist/cjs/apis/business/unblockOA.cjs +33 -0
- package/dist/cjs/apis/chat/preloadConversations.cjs +14 -0
- package/dist/cjs/apis/sync/downloadTransferSync.cjs +19 -1
- package/dist/cjs/apis/sync/getSyncData.cjs +20 -1
- package/dist/cjs/apis.cjs +20 -0
- package/dist/cjs/call-engine/ZaloCallTransport.cjs +28 -2
- package/dist/cjs/call-engine/ZaloRtcSession.cjs +9 -1
- package/dist/cjs/constants/defaults.cjs +2 -2
- package/dist/cjs/constants/protocol.cjs +1 -1
- package/dist/cjs/crypto/aes.cjs +3 -1
- package/dist/cjs/crypto/zdb-cipher.cjs +10 -3
- package/dist/cjs/index.cjs +10 -0
- package/dist/cjs/models/Message.cjs +16 -1
- package/dist/cjs/models/OAMessage.cjs +375 -0
- package/dist/cjs/realtime/handlers/control.handler.cjs +9 -1
- package/dist/cjs/realtime/handlers/message.handler.cjs +103 -37
- package/dist/cjs/realtime/listener.cjs +39 -18
- package/dist/cjs/utils.cjs +21 -2
- package/dist/cjs/zalo.cjs +21 -2
- package/dist/constants/defaults.d.ts +2 -2
- package/dist/constants/defaults.js +2 -2
- package/dist/constants/protocol.d.ts +1 -1
- package/dist/constants/protocol.js +1 -1
- package/dist/crypto/aes.js +3 -1
- package/dist/crypto/zdb-cipher.js +10 -3
- package/dist/index.d.ts +4 -0
- package/dist/models/Message.d.ts +12 -1
- package/dist/models/Message.js +16 -1
- package/dist/models/OAMessage.d.ts +221 -0
- package/dist/models/OAMessage.js +363 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/realtime/handlers/control.handler.js +9 -1
- package/dist/realtime/handlers/message.handler.js +103 -37
- package/dist/realtime/listener.d.ts +9 -3
- package/dist/realtime/listener.js +39 -18
- package/dist/realtime/types.d.ts +15 -1
- package/dist/utils.js +1 -1
- package/dist/zalo.d.ts +1 -1
- package/dist/zalo.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function safeJsonParse(input) {
|
|
4
|
+
if (input === null || input === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (typeof input === "object")
|
|
7
|
+
return input;
|
|
8
|
+
if (typeof input !== "string")
|
|
9
|
+
return undefined;
|
|
10
|
+
const trimmed = input.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(trimmed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function resolveHighlightValue(item) {
|
|
21
|
+
if (typeof item === "string")
|
|
22
|
+
return item;
|
|
23
|
+
if (typeof item === "number")
|
|
24
|
+
return String(item);
|
|
25
|
+
if (item && typeof item === "object") {
|
|
26
|
+
if (typeof item.dpn === "string" && item.dpn.length > 0)
|
|
27
|
+
return item.dpn;
|
|
28
|
+
if (typeof item.text === "string" && item.text.length > 0)
|
|
29
|
+
return item.text;
|
|
30
|
+
if (typeof item.value === "string" && item.value.length > 0)
|
|
31
|
+
return item.value;
|
|
32
|
+
if (typeof item.uid === "string" && item.uid.length > 0)
|
|
33
|
+
return item.uid;
|
|
34
|
+
if (typeof item.ts === "number")
|
|
35
|
+
return new Date(item.ts).toISOString();
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Điền các biến `%1$s`, `%2$s`... và `$1$s`, `$2$s`... trong mẫu chuỗi thông báo Zalo OA
|
|
41
|
+
* theo chuẩn hàm `_renderItemReplace` trên Zalo PC.
|
|
42
|
+
*/
|
|
43
|
+
function formatOAHighlightText(templateText, highLightsV2) {
|
|
44
|
+
if (!templateText || typeof templateText !== "string")
|
|
45
|
+
return "";
|
|
46
|
+
if (!Array.isArray(highLightsV2) || highLightsV2.length === 0)
|
|
47
|
+
return templateText;
|
|
48
|
+
let result = templateText;
|
|
49
|
+
highLightsV2.forEach((item, index) => {
|
|
50
|
+
const val = resolveHighlightValue(item);
|
|
51
|
+
const idx1 = index + 1;
|
|
52
|
+
result = result.replaceAll(`%${idx1}$s`, val).replaceAll(`$${idx1}$s`, val);
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Bóc tách cấu hình chân trang tương tác `footerv2` / `footer` của tin nhắn Zalo OA
|
|
58
|
+
* (`footerList`, `canForward`, `type`).
|
|
59
|
+
*/
|
|
60
|
+
function parseOAFooter(rawFooter) {
|
|
61
|
+
const parsed = safeJsonParse(rawFooter);
|
|
62
|
+
if (!parsed || typeof parsed !== "object")
|
|
63
|
+
return undefined;
|
|
64
|
+
const dataObj = safeJsonParse(parsed.data) ?? parsed;
|
|
65
|
+
const footerListRaw = Array.isArray(dataObj.footerList)
|
|
66
|
+
? dataObj.footerList
|
|
67
|
+
: Array.isArray(parsed.footerList)
|
|
68
|
+
? parsed.footerList
|
|
69
|
+
: [];
|
|
70
|
+
const footerList = footerListRaw
|
|
71
|
+
.map((entry) => safeJsonParse(entry))
|
|
72
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
73
|
+
const canForwardRaw = dataObj.canForward ?? parsed.canForward;
|
|
74
|
+
const canForward = canForwardRaw === undefined ? true : Boolean(Number(canForwardRaw));
|
|
75
|
+
const type = typeof parsed.type === "number" ? parsed.type : undefined;
|
|
76
|
+
return {
|
|
77
|
+
type,
|
|
78
|
+
canForward,
|
|
79
|
+
footerList,
|
|
80
|
+
raw: parsed,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Kiểm tra xem một hồ sơ người dùng có phải là Zalo Official Account (OA) hay không.
|
|
85
|
+
* Theo chuẩn `isOA(profile)` trên Zalo PC: `profile.type > 0` hoặc có `profile.oaInfo`.
|
|
86
|
+
*/
|
|
87
|
+
function isOAProfile(profile) {
|
|
88
|
+
if (!profile || typeof profile !== "object")
|
|
89
|
+
return false;
|
|
90
|
+
if (typeof profile.type === "number" && profile.type > 0)
|
|
91
|
+
return true;
|
|
92
|
+
return Boolean(profile.oaInfo && typeof profile.oaInfo === "object");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Kiểm tra xem Zalo OA có tích vàng xác thực (Verified OA) hay không.
|
|
96
|
+
* Theo chuẩn `isOAVerifiedByInfo(profile)` trên Zalo PC (`@10388120`):
|
|
97
|
+
* Chuyển `profile.type` sang nhị phân (`Number(type).toString(2)`), kiểm tra độ dài >= 3 và ký tự index `2` là `"1"`.
|
|
98
|
+
* Ví dụ: `type = 7` (`"111"`) -> Verified OA; `type = 5` (`"101"`) -> Verified OA.
|
|
99
|
+
*/
|
|
100
|
+
function isOAVerified(profileOrType) {
|
|
101
|
+
const typeNum = typeof profileOrType === "number"
|
|
102
|
+
? profileOrType
|
|
103
|
+
: typeof profileOrType?.type === "number"
|
|
104
|
+
? profileOrType.type
|
|
105
|
+
: 0;
|
|
106
|
+
if (!(typeNum > 0))
|
|
107
|
+
return false;
|
|
108
|
+
const bin = Number(typeNum).toString(2);
|
|
109
|
+
return bin.length >= 3 && bin[2] === "1";
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Kiểm tra xem tài khoản có phải là Zalo Bot hay không.
|
|
113
|
+
* Theo chuẩn `isZaloBotByInfo(profile)` trên Zalo PC (`@10388180`):
|
|
114
|
+
* Chuyển `profile.type` sang nhị phân (`Number(type).toString(2)`), kiểm tra độ dài >= 4 và ký tự đầu tiên là `"1"`.
|
|
115
|
+
*/
|
|
116
|
+
function isZaloBot(profileOrType) {
|
|
117
|
+
const typeNum = typeof profileOrType === "number"
|
|
118
|
+
? profileOrType
|
|
119
|
+
: typeof profileOrType?.type === "number"
|
|
120
|
+
? profileOrType.type
|
|
121
|
+
: 0;
|
|
122
|
+
if (!(typeNum > 0))
|
|
123
|
+
return false;
|
|
124
|
+
const bin = Number(typeNum).toString(2);
|
|
125
|
+
return bin.length >= 4 && bin[0] === "1";
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Giải mã trạng thái chặn & Do-Not-Disturb (`oa_status`) của Zalo OA
|
|
129
|
+
* theo chuẩn `_getOAStatus` trên Zalo PC (`@1719856`).
|
|
130
|
+
*/
|
|
131
|
+
function parseOADnrStatus(oaStatus, fallbackIsBlocked) {
|
|
132
|
+
const parsed = safeJsonParse(oaStatus);
|
|
133
|
+
const isBlocked = parsed && "blocked" in parsed
|
|
134
|
+
? Boolean(Number(parsed.blocked))
|
|
135
|
+
: Boolean(Number(fallbackIsBlocked ?? 0));
|
|
136
|
+
const enableDnr = parsed && "enable_dnr" in parsed ? Boolean(Number(parsed.enable_dnr)) : false;
|
|
137
|
+
const dnrType = parsed && typeof parsed.dnr_type === "number" ? parsed.dnr_type : null;
|
|
138
|
+
let dnrText = null;
|
|
139
|
+
if (parsed && parsed.dnr_string) {
|
|
140
|
+
if (typeof parsed.dnr_string === "string") {
|
|
141
|
+
dnrText = parsed.dnr_string;
|
|
142
|
+
}
|
|
143
|
+
else if (typeof parsed.dnr_string === "object" && parsed.dnr_string !== null) {
|
|
144
|
+
const langObj = parsed.dnr_string;
|
|
145
|
+
dnrText =
|
|
146
|
+
(typeof langObj.vi === "string" && langObj.vi) ||
|
|
147
|
+
(typeof langObj.en === "string" && langObj.en) ||
|
|
148
|
+
null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
isBlocked,
|
|
153
|
+
enableDnr,
|
|
154
|
+
dnrType,
|
|
155
|
+
dnrText,
|
|
156
|
+
raw: parsed ?? oaStatus ?? null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Chuẩn hóa dữ liệu hồ sơ Zalo Official Account (`getUserInfo` + `getBizAccount`) thành cấu trúc `OAProfileInfo` đồng nhất.
|
|
161
|
+
*/
|
|
162
|
+
function normalizeOAProfile(oaId, rawProfile, bizAccount) {
|
|
163
|
+
const prof = rawProfile ?? {};
|
|
164
|
+
const oaInfo = (safeJsonParse(prof.oaInfo) ?? null);
|
|
165
|
+
const biz = safeJsonParse(bizAccount?.biz) ?? {};
|
|
166
|
+
const type = typeof prof.type === "number" ? prof.type : oaInfo ? 1 : 0;
|
|
167
|
+
const dnrStatus = parseOADnrStatus(prof.oa_status, prof.isBlocked);
|
|
168
|
+
const pickFieldValue = (entry, fallback) => {
|
|
169
|
+
if (entry && typeof entry === "object" && typeof entry.value === "string" && entry.value.trim()) {
|
|
170
|
+
return entry.value.trim();
|
|
171
|
+
}
|
|
172
|
+
if (typeof fallback === "string" && fallback.trim()) {
|
|
173
|
+
return fallback.trim();
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
};
|
|
177
|
+
const cateObj = safeJsonParse(biz.cate);
|
|
178
|
+
const fallbackCategory = (typeof cateObj?.name === "string" && cateObj.name) ||
|
|
179
|
+
(typeof cateObj?.vi === "string" && cateObj.vi) ||
|
|
180
|
+
(typeof biz.cate === "string" ? biz.cate : "");
|
|
181
|
+
const displayName = (typeof prof.displayName === "string" && prof.displayName) ||
|
|
182
|
+
(typeof prof.zaloName === "string" && prof.zaloName) ||
|
|
183
|
+
oaId;
|
|
184
|
+
const zaloName = (typeof prof.zaloName === "string" && prof.zaloName) ||
|
|
185
|
+
displayName;
|
|
186
|
+
return {
|
|
187
|
+
oaId: String(prof.userId ?? oaId),
|
|
188
|
+
displayName,
|
|
189
|
+
zaloName,
|
|
190
|
+
username: typeof prof.username === "string" ? prof.username : String(prof.userId ?? oaId),
|
|
191
|
+
avatar: typeof prof.avatar === "string" ? prof.avatar : "",
|
|
192
|
+
cover: typeof prof.cover === "string" ? prof.cover : "",
|
|
193
|
+
globalId: typeof prof.globalId === "string" ? prof.globalId : "",
|
|
194
|
+
type,
|
|
195
|
+
isOA: isOAProfile({ type, oaInfo }),
|
|
196
|
+
isVerified: isOAVerified(type),
|
|
197
|
+
isZaloBot: isZaloBot(type),
|
|
198
|
+
isBlocked: dnrStatus.isBlocked,
|
|
199
|
+
isGroupedMsg: Number(oaInfo?.group_msg ?? 0) === 1,
|
|
200
|
+
description: (typeof oaInfo?.description === "string" && oaInfo.description.trim()) ||
|
|
201
|
+
(typeof biz.desc === "string" && biz.desc.trim()) ||
|
|
202
|
+
(typeof prof.status === "string" && prof.status.trim()) ||
|
|
203
|
+
"",
|
|
204
|
+
category: pickFieldValue(oaInfo?.category, fallbackCategory),
|
|
205
|
+
address: pickFieldValue(oaInfo?.address, biz.addr),
|
|
206
|
+
website: pickFieldValue(oaInfo?.website, biz.website),
|
|
207
|
+
hotline: pickFieldValue(oaInfo?.hotline, prof.phoneNumber),
|
|
208
|
+
openTime: pickFieldValue(oaInfo?.openTime),
|
|
209
|
+
email: typeof biz.email === "string" ? biz.email.trim() : "",
|
|
210
|
+
appUrl: typeof oaInfo?.appUrl === "string" ? oaInfo.appUrl : null,
|
|
211
|
+
dnrStatus,
|
|
212
|
+
oaInfo,
|
|
213
|
+
rawProfile: prof,
|
|
214
|
+
bizAccount: bizAccount ?? null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Bóc tách và giải mã các loại tin nhắn từ Zalo Official Account (OA):
|
|
219
|
+
* - Danh sách bài viết / thẻ hành động OA (`chat.list.action`, bao gồm `l.a.header.full`, `l.a.normal`)
|
|
220
|
+
* - Thông báo sự kiện OA (`webchat` với `action === "msginfo.actionlist"` hoặc `"msginfo.action"`)
|
|
221
|
+
* - Thiệp chúc mừng / sự kiện OA (`chat.ecard`)
|
|
222
|
+
* - Tin nhắn hội thoại trực tiếp 2 chiều với OA (`pageMsgs`, có `footerv2`, hoặc `topOut === "1"`)
|
|
223
|
+
*
|
|
224
|
+
* Trả về `undefined` nếu tin nhắn là tin nhắn cá nhân thông thường không thuộc Zalo OA.
|
|
225
|
+
*/
|
|
226
|
+
function parseOAMessage(msg, options) {
|
|
227
|
+
if (!msg || typeof msg.msgType !== "string")
|
|
228
|
+
return undefined;
|
|
229
|
+
const msgType = msg.msgType.trim();
|
|
230
|
+
const rawContent = msg.content;
|
|
231
|
+
const contentObj = typeof rawContent === "object" && rawContent !== null
|
|
232
|
+
? rawContent
|
|
233
|
+
: safeJsonParse(rawContent);
|
|
234
|
+
const footer = parseOAFooter(msg.footerv2 ?? msg.footer);
|
|
235
|
+
const topOut = msg.topOut === "1" || msg.topOut === 1 ? true : undefined;
|
|
236
|
+
// 1. Danh sách bài viết / Thẻ hành động OA (`chat.list.action`)
|
|
237
|
+
if (msgType === "chat.list.action") {
|
|
238
|
+
let items = [];
|
|
239
|
+
if (Array.isArray(rawContent)) {
|
|
240
|
+
items = rawContent;
|
|
241
|
+
}
|
|
242
|
+
else if (Array.isArray(contentObj)) {
|
|
243
|
+
items = contentObj;
|
|
244
|
+
}
|
|
245
|
+
else if (contentObj && typeof contentObj === "object") {
|
|
246
|
+
if (Array.isArray(contentObj.items)) {
|
|
247
|
+
items = contentObj.items;
|
|
248
|
+
}
|
|
249
|
+
else if (Array.isArray(contentObj.elements)) {
|
|
250
|
+
items = contentObj.elements;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const parsedParams = safeJsonParse(contentObj.params);
|
|
254
|
+
if (Array.isArray(parsedParams)) {
|
|
255
|
+
items = parsedParams;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
items = [contentObj];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const itemLines = items
|
|
263
|
+
.map((it) => {
|
|
264
|
+
const t = typeof it.title === "string" ? it.title.trim() : "";
|
|
265
|
+
const d = typeof it.description === "string" ? it.description.trim() : "";
|
|
266
|
+
if (t && d)
|
|
267
|
+
return `${t}: ${d}`;
|
|
268
|
+
return t || d;
|
|
269
|
+
})
|
|
270
|
+
.filter(Boolean);
|
|
271
|
+
return {
|
|
272
|
+
kind: "oa_list",
|
|
273
|
+
text: itemLines.join("\n"),
|
|
274
|
+
bundleData: contentObj ?? {},
|
|
275
|
+
items,
|
|
276
|
+
footer,
|
|
277
|
+
topOut,
|
|
278
|
+
canReply: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// 2. Tin nhắn thông báo / ActionList OA (`webchat` với `action === "msginfo.actionlist"` hoặc `"msginfo.action"`)
|
|
282
|
+
if ((msgType === "webchat" || msgType === "msginfo.actionlist" || msgType === "msginfo.action") &&
|
|
283
|
+
contentObj &&
|
|
284
|
+
(contentObj.action === "msginfo.actionlist" ||
|
|
285
|
+
contentObj.action === "msginfo.action" ||
|
|
286
|
+
msgType.startsWith("msginfo."))) {
|
|
287
|
+
const paramsObj = safeJsonParse(contentObj.params) ?? contentObj;
|
|
288
|
+
const highLights = paramsObj.highLightsV2;
|
|
289
|
+
const textVi = paramsObj.msg?.vi ? formatOAHighlightText(paramsObj.msg.vi, highLights) : undefined;
|
|
290
|
+
const textEn = paramsObj.msg?.en ? formatOAHighlightText(paramsObj.msg.en, highLights) : undefined;
|
|
291
|
+
const previewTxt = typeof paramsObj.previewTxt === "string"
|
|
292
|
+
? formatOAHighlightText(paramsObj.previewTxt, highLights)
|
|
293
|
+
: undefined;
|
|
294
|
+
const fallbackTitle = typeof contentObj.title === "string" ? contentObj.title : "";
|
|
295
|
+
const fallbackDesc = typeof contentObj.description === "string" ? contentObj.description : "";
|
|
296
|
+
return {
|
|
297
|
+
kind: "info_chat",
|
|
298
|
+
text: textVi || textEn || previewTxt || [fallbackTitle, fallbackDesc].filter(Boolean).join(" — "),
|
|
299
|
+
textVi,
|
|
300
|
+
textEn,
|
|
301
|
+
bundleData: paramsObj,
|
|
302
|
+
footer,
|
|
303
|
+
topOut,
|
|
304
|
+
metadata: { customMsg: paramsObj },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// 3. Thiệp chúc mừng / ECard OA (`chat.ecard`)
|
|
308
|
+
if (msgType === "chat.ecard") {
|
|
309
|
+
const paramsObj = safeJsonParse(contentObj?.params) ?? {};
|
|
310
|
+
const highLights = paramsObj.highLightsV2;
|
|
311
|
+
const textVi = paramsObj.msg?.vi ? formatOAHighlightText(paramsObj.msg.vi, highLights) : undefined;
|
|
312
|
+
const textEn = paramsObj.msg?.en ? formatOAHighlightText(paramsObj.msg.en, highLights) : undefined;
|
|
313
|
+
const previewTxt = typeof paramsObj.previewTxt === "string"
|
|
314
|
+
? formatOAHighlightText(paramsObj.previewTxt, highLights)
|
|
315
|
+
: undefined;
|
|
316
|
+
const fallbackTitle = typeof contentObj?.title === "string" ? contentObj.title.trim() : "";
|
|
317
|
+
const fallbackDesc = typeof contentObj?.description === "string" ? contentObj.description.trim() : "";
|
|
318
|
+
return {
|
|
319
|
+
kind: "ecard",
|
|
320
|
+
text: textVi || textEn || previewTxt || [fallbackTitle, fallbackDesc].filter(Boolean).join(" — "),
|
|
321
|
+
textVi,
|
|
322
|
+
textEn,
|
|
323
|
+
bundleData: (contentObj ?? paramsObj),
|
|
324
|
+
footer,
|
|
325
|
+
topOut,
|
|
326
|
+
metadata: { customMsg: paramsObj },
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// 4. Tin nhắn hội thoại trực tiếp 2 chiều với Zalo Official Account (`pageMsgs` / `footerv2` / `topOut === "1"`)
|
|
330
|
+
const hasOAMarkers = Boolean(options?.isPageMsg) || footer !== undefined || Boolean(topOut);
|
|
331
|
+
if (hasOAMarkers) {
|
|
332
|
+
let text = "";
|
|
333
|
+
if (typeof rawContent === "string") {
|
|
334
|
+
text = rawContent;
|
|
335
|
+
}
|
|
336
|
+
else if (contentObj && typeof contentObj === "object") {
|
|
337
|
+
const title = typeof contentObj.title === "string" ? contentObj.title.trim() : "";
|
|
338
|
+
const desc = typeof contentObj.description === "string" ? contentObj.description.trim() : "";
|
|
339
|
+
const href = typeof contentObj.href === "string" ? contentObj.href.trim() : "";
|
|
340
|
+
text = [title, desc].filter(Boolean).join(" — ") || href || JSON.stringify(contentObj);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
kind: "oa_chat",
|
|
344
|
+
text,
|
|
345
|
+
bundleData: contentObj ?? {},
|
|
346
|
+
footer,
|
|
347
|
+
topOut,
|
|
348
|
+
canReply: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
function initializeOAEvent(data, act) {
|
|
354
|
+
const uids = Array.isArray(data?.uids) ? data.uids.map(String) : [];
|
|
355
|
+
return {
|
|
356
|
+
act,
|
|
357
|
+
act_type: "oa",
|
|
358
|
+
data: {
|
|
359
|
+
...data,
|
|
360
|
+
uids,
|
|
361
|
+
},
|
|
362
|
+
isBlocked: act === "block",
|
|
363
|
+
oaIds: uids,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
exports.formatOAHighlightText = formatOAHighlightText;
|
|
368
|
+
exports.initializeOAEvent = initializeOAEvent;
|
|
369
|
+
exports.isOAProfile = isOAProfile;
|
|
370
|
+
exports.isOAVerified = isOAVerified;
|
|
371
|
+
exports.isZaloBot = isZaloBot;
|
|
372
|
+
exports.normalizeOAProfile = normalizeOAProfile;
|
|
373
|
+
exports.parseOADnrStatus = parseOADnrStatus;
|
|
374
|
+
exports.parseOAFooter = parseOAFooter;
|
|
375
|
+
exports.parseOAMessage = parseOAMessage;
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
var CallEvent = require('../../models/CallEvent.cjs');
|
|
4
4
|
var FriendEvent = require('../../models/FriendEvent.cjs');
|
|
5
5
|
var GroupEvent = require('../../models/GroupEvent.cjs');
|
|
6
|
+
var OAMessage = require('../../models/OAMessage.cjs');
|
|
6
7
|
var SyncEvent = require('../../models/SyncEvent.cjs');
|
|
7
8
|
var utils = require('../../utils.cjs');
|
|
8
9
|
var wsCipher = require('../../crypto/ws-cipher.cjs');
|
|
9
10
|
|
|
10
11
|
async function handleRealtimeControlPacket(listener, ctx, selfListen, version, cmd, subCmd, parsed, cipherKey) {
|
|
11
|
-
if (version == 1 && (cmd == 601 || cmd == 603 || cmd == 604) && subCmd == 0) {
|
|
12
|
+
if (version == 1 && (cmd == 601 || cmd == 602 || cmd == 603 || cmd == 604) && (subCmd == 0 || subCmd == 1)) {
|
|
12
13
|
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
13
14
|
const parsedData = decoded?.data;
|
|
14
15
|
const controls = parsedData?.controls;
|
|
@@ -89,6 +90,13 @@ async function handleRealtimeControlPacket(listener, ctx, selfListen, version, c
|
|
|
89
90
|
data: v2tData,
|
|
90
91
|
});
|
|
91
92
|
}
|
|
93
|
+
else if (control.content.act_type == "oa") {
|
|
94
|
+
const oaData = typeof control.content.data == "string"
|
|
95
|
+
? JSON.parse(control.content.data)
|
|
96
|
+
: control.content.data;
|
|
97
|
+
const oaEvent = OAMessage.initializeOAEvent(oaData, control.content.act);
|
|
98
|
+
listener.emit("oa_event", oaEvent);
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
return true;
|
|
94
102
|
}
|
|
@@ -17,64 +17,130 @@ require('../../models/CallEvent.cjs');
|
|
|
17
17
|
var utils = require('../../utils.cjs');
|
|
18
18
|
var wsCipher = require('../../crypto/ws-cipher.cjs');
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
function processIncomingUserMessages(listener, ctx, selfListen, onMessageCallback, messages, isPageMsg = false) {
|
|
21
|
+
for (const msg of messages) {
|
|
22
|
+
if (!msg)
|
|
23
|
+
continue;
|
|
24
|
+
if (typeof msg.content == "object" && msg.content !== null && utils.hasOwn(msg.content, "deleteMsg")) {
|
|
25
|
+
const undoObject = new Undo.Undo(ctx.uid, msg, false);
|
|
26
|
+
if (undoObject.isSelf && !selfListen)
|
|
27
|
+
continue;
|
|
28
|
+
listener.emit("undo", undoObject);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const messageObject = new Message.UserMessage(ctx.uid, msg, isPageMsg);
|
|
32
|
+
if (messageObject.isSelf && !selfListen)
|
|
33
|
+
continue;
|
|
34
|
+
onMessageCallback(messageObject);
|
|
35
|
+
listener.emit("message", messageObject);
|
|
36
|
+
if (messageObject.isOAMessage()) {
|
|
37
|
+
listener.emit("oa_message", messageObject);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function processIncomingGroupMessages(listener, ctx, selfListen, onMessageCallback, groupMessages) {
|
|
43
|
+
for (const msg of groupMessages) {
|
|
44
|
+
if (!msg)
|
|
45
|
+
continue;
|
|
46
|
+
if (typeof msg.content == "object" && msg.content !== null && utils.hasOwn(msg.content, "deleteMsg")) {
|
|
47
|
+
const undoObject = new Undo.Undo(ctx.uid, msg, true);
|
|
48
|
+
if (undoObject.isSelf && !selfListen)
|
|
49
|
+
continue;
|
|
50
|
+
listener.emit("undo", undoObject);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const messageObject = new Message.GroupMessage(ctx.uid, msg);
|
|
54
|
+
if (messageObject.isSelf && !selfListen)
|
|
55
|
+
continue;
|
|
56
|
+
onMessageCallback(messageObject);
|
|
57
|
+
listener.emit("message", messageObject);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function handleRealtimeMessagePacket(listener, ctx, selfListen, onMessageCallback, version, cmd, subCmd, parsed, cipherKey) {
|
|
62
|
+
if (version == 1 && [501, 551].includes(cmd) && subCmd == 0) {
|
|
63
|
+
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
64
|
+
const parsedData = decoded?.data ?? decoded;
|
|
65
|
+
const msgs = (Array.isArray(parsedData?.msgs) ? parsedData.msgs : []);
|
|
66
|
+
const pageMsgs = (Array.isArray(parsedData?.pageMsgs) ? parsedData.pageMsgs : []);
|
|
67
|
+
if (msgs.length > 0)
|
|
68
|
+
processIncomingUserMessages(listener, ctx, selfListen, onMessageCallback, msgs, false);
|
|
69
|
+
if (pageMsgs.length > 0)
|
|
70
|
+
processIncomingUserMessages(listener, ctx, selfListen, onMessageCallback, pageMsgs, true);
|
|
39
71
|
return true;
|
|
40
72
|
}
|
|
41
73
|
if (version == 1 && cmd == 521 && subCmd == 0) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
75
|
+
const parsedData = decoded?.data ?? decoded;
|
|
76
|
+
const groupMsgs = (Array.isArray(parsedData?.groupMsgs) ? parsedData.groupMsgs : []);
|
|
77
|
+
processIncomingGroupMessages(listener, ctx, selfListen, onMessageCallback, groupMsgs);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
// HAS_MSG (cmd = 533): Server báo có tin nhắn đang chờ (thường gặp với OA / Missed Queue) -> kéo ngay qua cmd = 570 (GET_MSG)
|
|
81
|
+
if (cmd == 533 && subCmd == 0) {
|
|
82
|
+
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
83
|
+
const parsedData = (decoded?.data ?? decoded);
|
|
84
|
+
if (parsedData?.queueId && Array.isArray(parsedData.ids) && parsedData.ids.length > 0) {
|
|
85
|
+
listener.requestMessagesByIds(parsedData.queueId, parsedData.ids);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// PUSH_MISS_MSG (cmd = 534) & GET_MSG (cmd = 570): Gói tin trả về các tin nhắn OA (`pageMsgs`), User (`msgs`) và Group (`groupMsgs`)
|
|
90
|
+
if ((cmd == 534 || cmd == 570) && subCmd == 0) {
|
|
91
|
+
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
92
|
+
const parsedData = decoded?.data ?? decoded;
|
|
93
|
+
const msgs = (Array.isArray(parsedData?.msgs) ? parsedData.msgs : []);
|
|
94
|
+
const pageMsgs = (Array.isArray(parsedData?.pageMsgs) ? parsedData.pageMsgs : []);
|
|
95
|
+
const groupMsgs = (Array.isArray(parsedData?.groupMsgs) ? parsedData.groupMsgs : []);
|
|
96
|
+
if (msgs.length > 0) {
|
|
97
|
+
processIncomingUserMessages(listener, ctx, selfListen, onMessageCallback, msgs, false);
|
|
98
|
+
}
|
|
99
|
+
if (pageMsgs.length > 0) {
|
|
100
|
+
processIncomingUserMessages(listener, ctx, selfListen, onMessageCallback, pageMsgs, true);
|
|
101
|
+
}
|
|
102
|
+
if (groupMsgs.length > 0) {
|
|
103
|
+
processIncomingGroupMessages(listener, ctx, selfListen, onMessageCallback, groupMsgs);
|
|
58
104
|
}
|
|
59
105
|
return true;
|
|
60
106
|
}
|
|
61
107
|
if (cmd == 510 && (subCmd == 0 || subCmd == 1)) {
|
|
62
108
|
const decoded = await wsCipher.decodeEventData(parsed, cipherKey);
|
|
63
109
|
const parsedData = decoded?.data ?? decoded;
|
|
64
|
-
const msgs = (parsedData?.msgs
|
|
110
|
+
const msgs = (Array.isArray(parsedData?.msgs) ? parsedData.msgs : []);
|
|
111
|
+
const pageMsgs = (Array.isArray(parsedData?.pageMsgs) ? parsedData.pageMsgs : []);
|
|
65
112
|
const responseMsgs = [];
|
|
66
113
|
for (const msg of msgs) {
|
|
67
|
-
if (typeof msg.content == "object" && utils.hasOwn(msg.content, "deleteMsg")) {
|
|
114
|
+
if (typeof msg.content == "object" && msg.content !== null && utils.hasOwn(msg.content, "deleteMsg")) {
|
|
115
|
+
const undoObject = new Undo.Undo(ctx.uid, msg, false);
|
|
116
|
+
if (undoObject.isSelf && !selfListen)
|
|
117
|
+
continue;
|
|
118
|
+
listener.emit("undo", undoObject);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const userMsg = new Message.UserMessage(ctx.uid, msg, false);
|
|
122
|
+
responseMsgs.push(userMsg);
|
|
123
|
+
if (userMsg.isOAMessage() && (!userMsg.isSelf || selfListen)) {
|
|
124
|
+
listener.emit("oa_message", userMsg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const msg of pageMsgs) {
|
|
129
|
+
if (typeof msg.content == "object" && msg.content !== null && utils.hasOwn(msg.content, "deleteMsg")) {
|
|
68
130
|
const undoObject = new Undo.Undo(ctx.uid, msg, false);
|
|
69
131
|
if (undoObject.isSelf && !selfListen)
|
|
70
132
|
continue;
|
|
71
133
|
listener.emit("undo", undoObject);
|
|
72
134
|
}
|
|
73
135
|
else {
|
|
74
|
-
|
|
136
|
+
const oaUserMsg = new Message.UserMessage(ctx.uid, msg, true);
|
|
137
|
+
responseMsgs.push(oaUserMsg);
|
|
138
|
+
if (oaUserMsg.isOAMessage() && (!oaUserMsg.isSelf || selfListen)) {
|
|
139
|
+
listener.emit("oa_message", oaUserMsg);
|
|
140
|
+
}
|
|
75
141
|
}
|
|
76
142
|
}
|
|
77
|
-
if (responseMsgs.length > 0 || msgs.length === 0) {
|
|
143
|
+
if (responseMsgs.length > 0 || (msgs.length === 0 && pageMsgs.length === 0)) {
|
|
78
144
|
listener.emit("old_messages", responseMsgs, Enum.ThreadType.User, parsedData?.more === 1);
|
|
79
145
|
}
|
|
80
146
|
return true;
|
|
@@ -330,18 +330,19 @@ class Listener extends EventEmitter {
|
|
|
330
330
|
}
|
|
331
331
|
sendWs(payload, requireId = true) {
|
|
332
332
|
if (this.ws && (this.ws.readyState === undefined || this.ws.readyState === WebSocket.OPEN)) {
|
|
333
|
-
if (requireId)
|
|
334
|
-
|
|
333
|
+
if (requireId) {
|
|
334
|
+
const reqId = `req_${this.id++}`;
|
|
335
|
+
payload.data["req_id"] = reqId;
|
|
336
|
+
payload.data["reqId"] = reqId;
|
|
337
|
+
}
|
|
335
338
|
const encodedData = new TextEncoder().encode(JSON.stringify(payload.data));
|
|
336
339
|
const dataLength = encodedData.length;
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
encodedData.
|
|
342
|
-
|
|
343
|
-
});
|
|
344
|
-
this.ws.send(data);
|
|
340
|
+
const buf = Buffer.alloc(4 + dataLength);
|
|
341
|
+
buf.writeUInt8(payload.version, 0);
|
|
342
|
+
buf.writeUInt16LE(payload.cmd, 1);
|
|
343
|
+
buf.writeInt8(payload.subCmd, 3);
|
|
344
|
+
Buffer.from(encodedData).copy(buf, 4);
|
|
345
|
+
this.ws.send(buf);
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
/**
|
|
@@ -376,16 +377,36 @@ class Listener extends EventEmitter {
|
|
|
376
377
|
this.sendWs(payload);
|
|
377
378
|
}
|
|
378
379
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
380
|
+
* Kéo danh sách tin nhắn cụ thể theo `queueId` và mảng `ids` (`cmd 570` GET_MSG)
|
|
381
|
+
* khi nhận tín hiệu `cmd 533` (`HAS_MSG`) từ Zalo WebSocket (thường dùng cho tin nhắn OA / ZNS / Missed Queue).
|
|
382
|
+
* Tự động chia batch tối đa 20 ID mỗi gói theo đúng cấu hình `max_msg_get = 20` trên Zalo Desktop.
|
|
383
|
+
*/
|
|
384
|
+
requestMessagesByIds(queueId, ids, lastId = "0") {
|
|
385
|
+
if (!queueId || !Array.isArray(ids) || ids.length === 0)
|
|
386
|
+
return;
|
|
387
|
+
const maxBatchSize = 20;
|
|
388
|
+
for (let i = 0; i < ids.length; i += maxBatchSize) {
|
|
389
|
+
const chunk = ids.slice(i, i + maxBatchSize);
|
|
390
|
+
const reqId = `req_${this.id++}`;
|
|
391
|
+
this.sendWs({
|
|
392
|
+
version: 1,
|
|
393
|
+
cmd: 570,
|
|
394
|
+
subCmd: 0,
|
|
395
|
+
data: {
|
|
396
|
+
queueId,
|
|
397
|
+
ids: chunk,
|
|
398
|
+
lastId,
|
|
399
|
+
reqId,
|
|
400
|
+
req_id: reqId,
|
|
401
|
+
},
|
|
402
|
+
}, false);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Kích hoạt và kéo hàng đợi sự kiện điều khiển / VoIP (`cmd 603` CTRL_PULL_CTRL).
|
|
407
|
+
* Giúp nhận ngay các cuộc gọi đến đang đổ chuông và sự kiện điều khiển ngay cả khi vừa mở kết nối WebSocket.
|
|
381
408
|
*/
|
|
382
409
|
requestOfflineVoipControls() {
|
|
383
|
-
this.sendWs({
|
|
384
|
-
version: 1,
|
|
385
|
-
cmd: 604,
|
|
386
|
-
subCmd: 0,
|
|
387
|
-
data: { first: true, lastId: 1, preIds: [] },
|
|
388
|
-
});
|
|
389
410
|
this.sendWs({
|
|
390
411
|
version: 1,
|
|
391
412
|
cmd: 603,
|