@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.
Files changed (51) hide show
  1. package/README.md +25 -2
  2. package/dist/apis/business/blockOA.d.ts +2 -0
  3. package/dist/apis/business/blockOA.js +28 -0
  4. package/dist/apis/business/convertOAIds.d.ts +4 -0
  5. package/dist/apis/business/convertOAIds.js +26 -0
  6. package/dist/apis/business/getOAProfile.d.ts +13 -0
  7. package/dist/apis/business/getOAProfile.js +64 -0
  8. package/dist/apis/business/unblockOA.d.ts +2 -0
  9. package/dist/apis/business/unblockOA.js +28 -0
  10. package/dist/apis/chat/preloadConversations.d.ts +3 -2
  11. package/dist/apis/chat/preloadConversations.js +14 -0
  12. package/dist/apis/sync/getSyncData.js +1 -1
  13. package/dist/apis.d.ts +12 -0
  14. package/dist/apis.js +20 -0
  15. package/dist/cjs/apis/business/blockOA.cjs +33 -0
  16. package/dist/cjs/apis/business/convertOAIds.cjs +31 -0
  17. package/dist/cjs/apis/business/getOAProfile.cjs +69 -0
  18. package/dist/cjs/apis/business/unblockOA.cjs +33 -0
  19. package/dist/cjs/apis/chat/preloadConversations.cjs +14 -0
  20. package/dist/cjs/apis/sync/getSyncData.cjs +20 -1
  21. package/dist/cjs/apis.cjs +20 -0
  22. package/dist/cjs/constants/defaults.cjs +2 -2
  23. package/dist/cjs/constants/protocol.cjs +1 -1
  24. package/dist/cjs/index.cjs +10 -0
  25. package/dist/cjs/models/Message.cjs +16 -1
  26. package/dist/cjs/models/OAMessage.cjs +375 -0
  27. package/dist/cjs/realtime/handlers/control.handler.cjs +9 -1
  28. package/dist/cjs/realtime/handlers/message.handler.cjs +103 -37
  29. package/dist/cjs/realtime/listener.cjs +39 -18
  30. package/dist/cjs/utils.cjs +21 -2
  31. package/dist/cjs/zalo.cjs +21 -2
  32. package/dist/constants/defaults.d.ts +2 -2
  33. package/dist/constants/defaults.js +2 -2
  34. package/dist/constants/protocol.d.ts +1 -1
  35. package/dist/constants/protocol.js +1 -1
  36. package/dist/index.d.ts +4 -0
  37. package/dist/models/Message.d.ts +12 -1
  38. package/dist/models/Message.js +16 -1
  39. package/dist/models/OAMessage.d.ts +221 -0
  40. package/dist/models/OAMessage.js +363 -0
  41. package/dist/models/index.d.ts +1 -0
  42. package/dist/models/index.js +1 -0
  43. package/dist/realtime/handlers/control.handler.js +9 -1
  44. package/dist/realtime/handlers/message.handler.js +103 -37
  45. package/dist/realtime/listener.d.ts +9 -3
  46. package/dist/realtime/listener.js +39 -18
  47. package/dist/realtime/types.d.ts +15 -1
  48. package/dist/utils.js +1 -1
  49. package/dist/zalo.d.ts +1 -1
  50. package/dist/zalo.js +1 -1
  51. 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
+ }
@@ -24,3 +24,4 @@ export * from "./ClearUnread.js";
24
24
  export * from "./SyncEvent.js";
25
25
  export * from "./SyncData.js";
26
26
  export * from "./CallEvent.js";
27
+ export * from "./OAMessage.js";
@@ -24,3 +24,4 @@ export * from "./ClearUnread.js";
24
24
  export * from "./SyncEvent.js";
25
25
  export * from "./SyncData.js";
26
26
  export * from "./CallEvent.js";
27
+ export * from "./OAMessage.js";
@@ -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
- export async function handleRealtimeMessagePacket(listener, ctx, selfListen, onMessageCallback, version, cmd, subCmd, parsed, cipherKey) {
4
- if (version == 1 && [501, 551].includes(cmd) && subCmd == 0) {
5
- const parsedData = (await decodeEventData(parsed, cipherKey)).data;
6
- const { msgs } = parsedData;
7
- for (const msg of msgs) {
8
- if (typeof msg.content == "object" && hasOwn(msg.content, "deleteMsg")) {
9
- const undoObject = new Undo(ctx.uid, msg, false);
10
- if (undoObject.isSelf && !selfListen)
11
- continue;
12
- listener.emit("undo", undoObject);
13
- }
14
- else {
15
- const messageObject = new UserMessage(ctx.uid, msg);
16
- if (messageObject.isSelf && !selfListen)
17
- continue;
18
- onMessageCallback(messageObject);
19
- listener.emit("message", messageObject);
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 parsedData = (await decodeEventData(parsed, cipherKey)).data;
26
- const { groupMsgs } = parsedData;
27
- for (const msg of groupMsgs) {
28
- if (typeof msg.content == "object" && hasOwn(msg.content, "deleteMsg")) {
29
- const undoObject = new Undo(ctx.uid, msg, true);
30
- if (undoObject.isSelf && !selfListen)
31
- continue;
32
- listener.emit("undo", undoObject);
33
- }
34
- else {
35
- const messageObject = new GroupMessage(ctx.uid, msg);
36
- if (messageObject.isSelf && !selfListen)
37
- continue;
38
- onMessageCallback(messageObject);
39
- listener.emit("message", messageObject);
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
- responseMsgs.push(new UserMessage(ctx.uid, msg));
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
- * Kích hoạt và kéo hàng đợi sự kiện điều khiển / VoIP (`cmd 603` CTRL_PULL_CTRL và `cmd 604` CTRL_PULL_VOIP).
71
- * Giúp nhận ngay các cuộc gọi đến đang đổ chuông ngay cả khi vừa mở kết nối WebSocket.
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;