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