@softtynet/zca-js 2.2.1 → 2.2.3

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 (94) hide show
  1. package/README.md +47 -21
  2. package/dist/apis/cancelPullMobile.d.ts +11 -0
  3. package/dist/apis/cancelPullMobile.js +33 -0
  4. package/dist/apis/cancelVoiceCall.d.ts +7 -0
  5. package/dist/apis/cancelVoiceCall.js +43 -0
  6. package/dist/apis/decryptGids.d.ts +2 -1
  7. package/dist/apis/decryptGids.js +2 -0
  8. package/dist/apis/endVoiceCall.d.ts +5 -0
  9. package/dist/apis/endVoiceCall.js +41 -0
  10. package/dist/apis/getNoisedIds.d.ts +9 -0
  11. package/dist/apis/getNoisedIds.js +67 -0
  12. package/dist/apis/getSyncData.d.ts +13 -0
  13. package/dist/apis/getSyncData.js +103 -0
  14. package/dist/apis/listen.d.ts +11 -3
  15. package/dist/apis/listen.js +71 -14
  16. package/dist/apis/preloadConversations.d.ts +20 -2
  17. package/dist/apis/preloadConversations.js +56 -3
  18. package/dist/apis/pullMobile.d.ts +15 -0
  19. package/dist/apis/pullMobile.js +38 -0
  20. package/dist/apis/requestSyncFromPhone.d.ts +17 -1
  21. package/dist/apis/requestSyncFromPhone.js +34 -20
  22. package/dist/apis/sendVoiceCall.d.ts +29 -0
  23. package/dist/apis/sendVoiceCall.js +105 -0
  24. package/dist/apis/uploadAttachment.js +6 -1
  25. package/dist/apis.d.ts +14 -0
  26. package/dist/apis.js +14 -0
  27. package/dist/cjs/apis/addReaction.cjs +2 -0
  28. package/dist/cjs/apis/addUnreadMark.cjs +2 -0
  29. package/dist/cjs/apis/cancelPullMobile.cjs +38 -0
  30. package/dist/cjs/apis/cancelVoiceCall.cjs +48 -0
  31. package/dist/cjs/apis/createReminder.cjs +2 -0
  32. package/dist/cjs/apis/decryptGids.cjs +2 -0
  33. package/dist/cjs/apis/deleteChat.cjs +2 -0
  34. package/dist/cjs/apis/deleteMessage.cjs +2 -0
  35. package/dist/cjs/apis/editReminder.cjs +2 -0
  36. package/dist/cjs/apis/endVoiceCall.cjs +46 -0
  37. package/dist/cjs/apis/findUser.cjs +2 -0
  38. package/dist/cjs/apis/findUserByUsername.cjs +2 -0
  39. package/dist/cjs/apis/forwardMessage.cjs +2 -0
  40. package/dist/cjs/apis/getAllFriends.cjs +2 -0
  41. package/dist/cjs/apis/getAvatarUrlProfile.cjs +2 -0
  42. package/dist/cjs/apis/getGroupChatHistory.cjs +2 -0
  43. package/dist/cjs/apis/getListBoard.cjs +2 -0
  44. package/dist/cjs/apis/getListReminder.cjs +2 -0
  45. package/dist/cjs/apis/getMultiUsersByPhones.cjs +2 -0
  46. package/dist/cjs/apis/getNoisedIds.cjs +72 -0
  47. package/dist/cjs/apis/getSyncData.cjs +108 -0
  48. package/dist/cjs/apis/getUserInfo.cjs +2 -0
  49. package/dist/cjs/apis/listen.cjs +71 -14
  50. package/dist/cjs/apis/preloadConversations.cjs +69 -3
  51. package/dist/cjs/apis/pullMobile.cjs +43 -0
  52. package/dist/cjs/apis/removeReminder.cjs +2 -0
  53. package/dist/cjs/apis/removeUnreadMark.cjs +2 -0
  54. package/dist/cjs/apis/requestSyncFromPhone.cjs +33 -19
  55. package/dist/cjs/apis/sendBankCard.cjs +2 -0
  56. package/dist/cjs/apis/sendCard.cjs +2 -0
  57. package/dist/cjs/apis/sendDeliveredEvent.cjs +2 -0
  58. package/dist/cjs/apis/sendLink.cjs +2 -0
  59. package/dist/cjs/apis/sendMessage.cjs +2 -0
  60. package/dist/cjs/apis/sendReport.cjs +2 -0
  61. package/dist/cjs/apis/sendSeenEvent.cjs +2 -0
  62. package/dist/cjs/apis/sendSticker.cjs +2 -0
  63. package/dist/cjs/apis/sendTypingEvent.cjs +2 -0
  64. package/dist/cjs/apis/sendVideo.cjs +2 -0
  65. package/dist/cjs/apis/sendVoice.cjs +2 -0
  66. package/dist/cjs/apis/sendVoiceCall.cjs +110 -0
  67. package/dist/cjs/apis/setHiddenConversations.cjs +2 -0
  68. package/dist/cjs/apis/setMute.cjs +2 -0
  69. package/dist/cjs/apis/setPinnedConversations.cjs +2 -0
  70. package/dist/cjs/apis/undo.cjs +2 -0
  71. package/dist/cjs/apis/updateAutoDeleteChat.cjs +2 -0
  72. package/dist/cjs/apis/uploadAttachment.cjs +8 -1
  73. package/dist/cjs/apis.cjs +14 -0
  74. package/dist/cjs/index.cjs +24 -0
  75. package/dist/cjs/models/CallEvent.cjs +51 -0
  76. package/dist/cjs/models/ClearUnread.cjs +17 -0
  77. package/dist/cjs/models/SyncEvent.cjs +54 -0
  78. package/dist/cjs/utils.cjs +153 -1
  79. package/dist/index.d.ts +11 -1
  80. package/dist/index.js +2 -0
  81. package/dist/models/CallEvent.d.ts +30 -0
  82. package/dist/models/CallEvent.js +47 -0
  83. package/dist/models/ClearUnread.d.ts +22 -0
  84. package/dist/models/ClearUnread.js +12 -0
  85. package/dist/models/SyncData.d.ts +35 -0
  86. package/dist/models/SyncData.js +1 -0
  87. package/dist/models/SyncEvent.d.ts +83 -0
  88. package/dist/models/SyncEvent.js +50 -0
  89. package/dist/models/index.d.ts +4 -0
  90. package/dist/models/index.js +4 -0
  91. package/dist/utils.d.ts +34 -0
  92. package/dist/utils.js +148 -1
  93. package/docs/REA_REVERSE_ENGINEERING_PLAN.md +140 -0
  94. package/package.json +3 -1
package/README.md CHANGED
@@ -115,44 +115,70 @@ api.listener.start();
115
115
  > [!IMPORTANT]
116
116
  > Only one web listener can run per account at a time. If you open Zalo in the browser while the listener is active, the listener will be automatically stopped.
117
117
 
118
- ### Synchronize Messages from Mobile
118
+ ### Synchronize Messages & Offline Catch-up
119
119
 
120
- `zca-js` supports cross-device message synchronization from the official Zalo mobile app:
121
-
122
- #### Case 1: Auto-Sync on QR Login
123
- When logging in via `loginQR`, check the **"Sync message with your phone"** checkbox on your phone before confirming:
120
+ `zca-js` supports both **offline message catch-up** (when reconnecting or restarting your app) and **full mobile backup synchronization** (`ZDB4.0`) from the Zalo mobile app:
124
121
 
122
+ #### 1. Catch Up Missed Messages After Reconnect / Restart
125
123
  ```javascript
126
- import { Zalo, LoginQRCallbackEventType } from "zca-js";
124
+ import { Zalo, ThreadType } from "zca-js";
127
125
 
128
- const zalo = new Zalo({ selfListen: true });
129
- const api = await zalo.loginQR({ qrPath: "./qr.png" });
126
+ const preload = await api.preloadConversations();
127
+ console.log(`Preloaded ${preload.messages.length} latest messages`);
130
128
 
131
- // Listen for historical messages pushed from phone
132
- api.listener.on("old_messages", (messages, type) => {
133
- console.log(`Received ${messages.length} historical synced messages!`);
129
+ api.listener.on("old_messages", (messages, type, hasMore) => {
130
+ console.log(`Received ${messages.length} queued offline messages (type=${type}, hasMore=${hasMore})`);
134
131
  });
135
132
 
136
- api.listener.start();
137
- ```
133
+ api.listener.on("cipher_key", () => {
134
+ api.listener.requestOldMessages(ThreadType.User);
135
+ api.listener.requestOldMessages(ThreadType.Group);
136
+ });
138
137
 
139
- #### Case 2: Manual Trigger Sync (from existing session)
140
- To request message synchronization from your phone at any time:
138
+ api.listener.start({ retryOnClose: true });
139
+ ```
141
140
 
141
+ #### 2. Trigger Mobile Backup Sync (`pull_mobile_msg`)
142
142
  ```javascript
143
- api.listener.on("connected", async () => {
144
- // Send sync request signal to mobile phone
145
- await api.requestSyncFromPhone();
146
- console.log("Please tap 'Sync now' on your phone!");
143
+ import { SyncEventType, decryptWithPrivateKey, deriveZdbAesKey, decryptZdbBuffer } from "zca-js";
144
+
145
+ let privateKey = "";
146
+
147
+ api.listener.on("sync_event", async (event) => {
148
+ if (event.type === SyncEventType.USER_CONFIRM) {
149
+ console.log("User confirmed sync on phone, waiting for backup upload...");
150
+ } else if (event.type === SyncEventType.SYNCMSG_INFO) {
151
+ const res = await api.getSyncData({ ...event.data, savePath: "./sync_data" });
152
+ if (res.success && res.data && privateKey) {
153
+ const rawRsa = decryptWithPrivateKey(privateKey, res.data.encrypted_key);
154
+ const aesKey = deriveZdbAesKey(rawRsa);
155
+ console.log("Downloaded backup:", res.data.file_path, "AES key length:", aesKey.length);
156
+ }
157
+ }
147
158
  });
148
159
 
149
- api.listener.on("old_messages", (messages, type) => {
150
- console.log(`Received ${messages.length} synced messages from phone!`);
160
+ api.listener.on("connected", async () => {
161
+ const session = await api.requestSyncFromPhone();
162
+ privateKey = session.privateKey;
163
+ console.log("Please tap 'Sync now' on your phone!");
151
164
  });
152
165
 
153
166
  api.listener.start();
154
167
  ```
155
168
 
169
+ ### Voice / Video Call Signaling
170
+
171
+ ```javascript
172
+ import { CallType } from "zca-js";
173
+
174
+ // Ring a user's phone (Audio or Video call)
175
+ const call = await api.sendVoiceCall("target_user_id", { callType: CallType.Audio });
176
+ console.log("Ringing callId:", call.callId, "session:", call.sessId);
177
+
178
+ // Cancel ringing or end an active call
179
+ await api.cancelVoiceCall("target_user_id", call.callId);
180
+ ```
181
+
156
182
  ### Send a message
157
183
 
158
184
  ```javascript
@@ -0,0 +1,11 @@
1
+ export type CancelPullMobileResponse = {
2
+ error_code?: number | string;
3
+ error_message?: string;
4
+ data?: string;
5
+ [key: string]: unknown;
6
+ };
7
+ export type CancelPullMobileParams = {
8
+ public_key: string;
9
+ pc_name?: string;
10
+ };
11
+ export declare const cancelPullMobileFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => ({ public_key, pc_name }: CancelPullMobileParams) => Promise<CancelPullMobileResponse>;
@@ -0,0 +1,33 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const cancelPullMobileFactory = apiFactory()((api, ctx, utils) => {
4
+ const baseURL = utils.makeURL(`${api.zpwServiceMap.file[0]}/api/message/cancel_pull_mobile_msg`);
5
+ /**
6
+ * Huỷ yêu cầu đồng bộ tin nhắn từ điện thoại đang chờ (`cancel_pull_mobile_msg`, command `12700`).
7
+ *
8
+ * @param params Tham số huỷ đồng bộ (`public_key`, `pc_name`)
9
+ * @throws {ZaloApiError}
10
+ */
11
+ return async function cancelPullMobile({ public_key, pc_name = "Web" }) {
12
+ if (!public_key)
13
+ throw new ZaloApiError("Missing public_key");
14
+ const payload = {
15
+ pc_name,
16
+ public_key,
17
+ imei: ctx.imei,
18
+ };
19
+ const encryptedParams = utils.encodeAES(JSON.stringify(payload));
20
+ if (!encryptedParams)
21
+ throw new ZaloApiError("Failed to encrypt params");
22
+ const serviceURL = utils.makeURL(baseURL, {
23
+ zpw_ver: ctx.API_VERSION,
24
+ zpw_type: ctx.API_TYPE,
25
+ params: encryptedParams,
26
+ nretry: 0,
27
+ }, false);
28
+ const response = await utils.request(serviceURL, {
29
+ method: "GET",
30
+ });
31
+ return utils.resolve(response);
32
+ };
33
+ });
@@ -0,0 +1,7 @@
1
+ import { CallType } from "./sendVoiceCall.js";
2
+ export type CancelVoiceCallOptions = {
3
+ callType?: CallType;
4
+ status?: number;
5
+ };
6
+ export type CancelVoiceCallResponse = Record<string, unknown>;
7
+ export declare const cancelVoiceCallFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (calleeId: string, callId: number, options?: CancelVoiceCallOptions) => Promise<CancelVoiceCallResponse>;
@@ -0,0 +1,43 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ import { CallType } from "./sendVoiceCall.js";
4
+ export const cancelVoiceCallFactory = apiFactory()((api, ctx, utils) => {
5
+ var _a;
6
+ const voiceCallService = (_a = api.zpwServiceMap.voice_call) === null || _a === void 0 ? void 0 : _a[0];
7
+ /**
8
+ * Huỷ cuộc gọi đang đổ chuông trước khi người nhận bắt máy (`/api/voicecall/cancel`, command `11305`).
9
+ *
10
+ * @param calleeId User ID của người nhận cuộc gọi
11
+ * @param callId ID của cuộc gọi (nhận từ `sendVoiceCall`)
12
+ * @param options Tuỳ chọn loại cuộc gọi (`callType`) và `status`
13
+ * @throws {ZaloApiError}
14
+ */
15
+ return async function cancelVoiceCall(calleeId, callId, options = {}) {
16
+ var _a, _b;
17
+ if (!calleeId)
18
+ throw new ZaloApiError("Missing calleeId");
19
+ if (!callId)
20
+ throw new ZaloApiError("Missing callId");
21
+ if (!voiceCallService)
22
+ throw new ZaloApiError("Missing voice_call service URL in zpwServiceMap");
23
+ const numericCalleeId = Number.isNaN(Number(calleeId)) ? calleeId : Number(calleeId);
24
+ const params = {
25
+ callerId: numericCalleeId,
26
+ callId,
27
+ callType: (_a = options.callType) !== null && _a !== void 0 ? _a : CallType.Audio,
28
+ status: (_b = options.status) !== null && _b !== void 0 ? _b : 0,
29
+ imei: ctx.imei,
30
+ };
31
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
32
+ if (!encryptedParams)
33
+ throw new ZaloApiError("Failed to encrypt cancelVoiceCall params");
34
+ const url = utils.makeURL(`${voiceCallService}/api/voicecall/cancel`, {
35
+ zpw_type: 24,
36
+ params: encryptedParams,
37
+ });
38
+ const response = await utils.request(url, {
39
+ method: "GET",
40
+ });
41
+ return utils.resolve(response);
42
+ };
43
+ });
@@ -1,4 +1,5 @@
1
1
  export type DecryptGidsResponse = {
2
- gids: Record<string, string>;
2
+ gids?: Record<string, string>;
3
+ [key: string]: unknown;
3
4
  };
4
5
  export declare const decryptGidsFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (gids: string | string[]) => Promise<DecryptGidsResponse>;
@@ -4,6 +4,7 @@ export const decryptGidsFactory = apiFactory()((api, ctx, utils) => {
4
4
  const serviceURL = utils.makeURL(`${api.zpwServiceMap.profile[0]}/api/gid/decrypt`);
5
5
  /**
6
6
  * Giải mã danh sách Global ID (g_...) ẩn danh thành User ID thật
7
+ * (tương ứng hàm `getUidFromGlobalNoiseId` / command `12054` trên Zalo PC)
7
8
  *
8
9
  * @param gids Danh sách Global ID (hoặc 1 GID đơn lẻ)
9
10
  * @throws {ZaloApiError}
@@ -14,6 +15,7 @@ export const decryptGidsFactory = apiFactory()((api, ctx, utils) => {
14
15
  if (!Array.isArray(gids))
15
16
  gids = [gids];
16
17
  const params = {
18
+ globalUids: JSON.stringify(gids),
17
19
  gids,
18
20
  imei: ctx.imei,
19
21
  };
@@ -0,0 +1,5 @@
1
+ export type EndVoiceCallOptions = {
2
+ status?: number;
3
+ };
4
+ export type EndVoiceCallResponse = Record<string, unknown>;
5
+ export declare const endVoiceCallFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (calleeId: string, callId: number, options?: EndVoiceCallOptions) => Promise<EndVoiceCallResponse>;
@@ -0,0 +1,41 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const endVoiceCallFactory = apiFactory()((api, ctx, utils) => {
4
+ var _a;
5
+ const voiceCallService = (_a = api.zpwServiceMap.voice_call) === null || _a === void 0 ? void 0 : _a[0];
6
+ /**
7
+ * Kết thúc cuộc gọi thoại / video (`/api/voicecall/endcall`, command `11306`).
8
+ *
9
+ * @param calleeId User ID của đối phương
10
+ * @param callId ID của cuộc gọi
11
+ * @param options Tuỳ chọn mã trạng thái kết thúc (`status`, mặc định `3`)
12
+ * @throws {ZaloApiError}
13
+ */
14
+ return async function endVoiceCall(calleeId, callId, options = {}) {
15
+ var _a;
16
+ if (!calleeId)
17
+ throw new ZaloApiError("Missing calleeId");
18
+ if (!callId)
19
+ throw new ZaloApiError("Missing callId");
20
+ if (!voiceCallService)
21
+ throw new ZaloApiError("Missing voice_call service URL in zpwServiceMap");
22
+ const numericCalleeId = Number.isNaN(Number(calleeId)) ? calleeId : Number(calleeId);
23
+ const params = {
24
+ uidTo: numericCalleeId,
25
+ callId,
26
+ status: (_a = options.status) !== null && _a !== void 0 ? _a : 3,
27
+ imei: ctx.imei,
28
+ };
29
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
30
+ if (!encryptedParams)
31
+ throw new ZaloApiError("Failed to encrypt endVoiceCall params");
32
+ const url = utils.makeURL(`${voiceCallService}/api/voicecall/endcall`, {
33
+ zpw_type: 24,
34
+ params: encryptedParams,
35
+ });
36
+ const response = await utils.request(url, {
37
+ method: "GET",
38
+ });
39
+ return utils.resolve(response);
40
+ };
41
+ });
@@ -0,0 +1,9 @@
1
+ export type GetNoisedIdsParams = {
2
+ fids?: Array<string | number>;
3
+ gids?: Array<string | number>;
4
+ };
5
+ export type GetNoisedIdsResponse = {
6
+ fids: string[];
7
+ gids: string[];
8
+ };
9
+ export declare const getNoisedIdsFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => ({ fids, gids }: GetNoisedIdsParams) => Promise<GetNoisedIdsResponse>;
@@ -0,0 +1,67 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ const ZWID_ZNOISE_URL = "https://zwid.api.zalo.me/api/znoise";
4
+ function toNumericIds(ids) {
5
+ return ids.map((id) => Number.parseInt(String(id), 10));
6
+ }
7
+ function normalizeNoised(list, expected, stripGroupPrefix) {
8
+ const out = [];
9
+ const arr = Array.isArray(list) ? list : [];
10
+ for (let i = 0; i < expected; i++) {
11
+ let v = arr[i] == null ? "" : String(arr[i]);
12
+ if (stripGroupPrefix && v.startsWith("g"))
13
+ v = v.slice(1);
14
+ out.push(v === "0" ? "" : v);
15
+ }
16
+ return out;
17
+ }
18
+ export const getNoisedIdsFactory = apiFactory()((api, ctx, utils) => {
19
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.profile[0]}/api/znoise`);
20
+ /**
21
+ * Chuyển đổi danh sách UID cá nhân (`fids`) và GID nhóm (`gids`) dạng số thuần (từ SQLite ZDB4.0 mobile)
22
+ * sang định dạng Noised ID 19 chữ số dùng trên Zalo Web/PC (`getNoisedUids`, command `12390`).
23
+ * Tự động fallback sang `https://zwid.api.zalo.me/api/znoise` nếu profile domain trả về lỗi.
24
+ *
25
+ * @param params Danh sách `fids` và `gids`
26
+ * @throws {ZaloApiError}
27
+ */
28
+ return async function getNoisedIds({ fids = [], gids = [] }) {
29
+ const payload = {
30
+ fids: toNumericIds(fids),
31
+ gids: toNumericIds(gids),
32
+ };
33
+ const encryptedParams = utils.encodeAES(JSON.stringify(payload));
34
+ if (!encryptedParams)
35
+ throw new ZaloApiError("Failed to encrypt params");
36
+ try {
37
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
38
+ method: "GET",
39
+ });
40
+ const data = await utils.resolve(response);
41
+ const raw = (data !== null && data !== void 0 ? data : {});
42
+ return {
43
+ fids: normalizeNoised(raw.fids, fids.length, false),
44
+ gids: normalizeNoised(raw.gids, gids.length, true),
45
+ };
46
+ }
47
+ catch (primaryErr) {
48
+ try {
49
+ const body = new URLSearchParams({ params: encryptedParams }).toString();
50
+ const fallbackUrl = utils.makeURL(ZWID_ZNOISE_URL);
51
+ const response = await utils.request(fallbackUrl, {
52
+ method: "POST",
53
+ body,
54
+ });
55
+ const data = await utils.resolve(response);
56
+ const raw = (data !== null && data !== void 0 ? data : {});
57
+ return {
58
+ fids: normalizeNoised(raw.fids, fids.length, false),
59
+ gids: normalizeNoised(raw.gids, gids.length, true),
60
+ };
61
+ }
62
+ catch (_a) {
63
+ throw primaryErr;
64
+ }
65
+ }
66
+ };
67
+ });
@@ -0,0 +1,13 @@
1
+ import type { SyncDataResponse } from "../models/index.js";
2
+ export type GetSyncDataParams = {
3
+ url: string;
4
+ encrypted_key: string;
5
+ file_name: string;
6
+ file_size: number;
7
+ checksum_code: string;
8
+ from_seq_id: number;
9
+ is_full_transfer: number;
10
+ returnStream?: boolean;
11
+ savePath?: string;
12
+ };
13
+ export declare const getSyncDataFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => ({ url, encrypted_key, file_name, file_size, checksum_code, from_seq_id, is_full_transfer, returnStream, savePath, }: GetSyncDataParams) => Promise<SyncDataResponse>;
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import toughCookie from "tough-cookie";
4
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
5
+ import { apiFactory } from "../utils.js";
6
+ export const getSyncDataFactory = apiFactory()((api, ctx, utils) => {
7
+ async function downloadFileStream(url) {
8
+ if (!ctx.cookie)
9
+ ctx.cookie = new toughCookie.CookieJar();
10
+ const cookie = ctx.cookie;
11
+ const manualHeaders = {
12
+ "Cache-Control": "no-store, must-revalidate",
13
+ Pragma: "no-cache",
14
+ "Accept-Encoding": "gzip, deflate, br",
15
+ "Accept-Language": "en-US",
16
+ "User-Agent": ctx.userAgent || "",
17
+ cookie: await cookie.getCookieString("https://chat.zalo.me/"),
18
+ Expires: "0",
19
+ "Sec-Fetch-Dest": "empty",
20
+ "Sec-Fetch-Mode": "no-cors",
21
+ "Sec-Fetch-Site": "none",
22
+ Connection: "keep-alive",
23
+ };
24
+ const response = await utils.request(url, {
25
+ method: "GET",
26
+ headers: manualHeaders,
27
+ }, true);
28
+ if (!response.ok) {
29
+ throw new ZaloApiError(`Failed to download sync file: HTTP ${response.status}`);
30
+ }
31
+ return response;
32
+ }
33
+ async function downloadFile(url) {
34
+ const response = await downloadFileStream(url);
35
+ const arrayBuffer = await response.arrayBuffer();
36
+ return Buffer.from(arrayBuffer);
37
+ }
38
+ /**
39
+ * Tải xuống file sao lưu tin nhắn từ sự kiện `syncmsg_info` (`.db.crypt`, định dạng `ZDB4.0`)
40
+ * và lưu ra ổ đĩa hoặc trả về `ReadableStream`.
41
+ *
42
+ * @param params Thông tin file sao lưu nhận được từ `SyncEventType.SYNCMSG_INFO`
43
+ * @returns `Promise<SyncDataResponse>`
44
+ */
45
+ return async function getSyncData({ url, encrypted_key, file_name, file_size, checksum_code, from_seq_id, is_full_transfer, returnStream = false, savePath = "./", }) {
46
+ try {
47
+ if (returnStream) {
48
+ const response = await downloadFileStream(url);
49
+ const syncDataStream = {
50
+ stream: response.body,
51
+ metadata: {
52
+ file_name,
53
+ file_size,
54
+ checksum_code,
55
+ from_seq_id,
56
+ is_full_transfer,
57
+ encrypted_key,
58
+ },
59
+ response: {
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ url: response.url,
63
+ contentType: response.headers.get("content-type"),
64
+ contentLength: response.headers.get("content-length"),
65
+ },
66
+ };
67
+ return {
68
+ success: true,
69
+ stream: syncDataStream,
70
+ };
71
+ }
72
+ const encryptedFileData = await downloadFile(url);
73
+ const fileName = file_name.endsWith(".db.crypt") ? file_name : `${file_name}.db.crypt`;
74
+ const fullPath = path.join(savePath, fileName);
75
+ const dir = path.dirname(fullPath);
76
+ if (!fs.existsSync(dir)) {
77
+ fs.mkdirSync(dir, { recursive: true });
78
+ }
79
+ fs.writeFileSync(fullPath, encryptedFileData);
80
+ const syncData = {
81
+ file_path: fullPath,
82
+ encrypted_key,
83
+ metadata: {
84
+ file_name: fileName,
85
+ file_size,
86
+ checksum_code,
87
+ from_seq_id,
88
+ is_full_transfer,
89
+ },
90
+ };
91
+ return {
92
+ success: true,
93
+ data: syncData,
94
+ };
95
+ }
96
+ catch (error) {
97
+ return {
98
+ success: false,
99
+ error: error instanceof Error ? error.message : "Unknown error occurred",
100
+ };
101
+ }
102
+ };
103
+ });
@@ -6,6 +6,9 @@ import { Reaction, Undo, ThreadType } from "../models/index.js";
6
6
  import type { ContextSession } from "../context.js";
7
7
  import { type SeenMessage } from "../models/SeenMessage.js";
8
8
  import { type DeliveredMessage } from "../models/DeliveredMessage.js";
9
+ import { ClearUnread } from "../models/ClearUnread.js";
10
+ import { type SyncEvent } from "../models/SyncEvent.js";
11
+ import { type CallEvent } from "../models/CallEvent.js";
9
12
  type UploadEventData = {
10
13
  fileUrl: string;
11
14
  fileId: string;
@@ -33,15 +36,18 @@ interface ListenerEvents {
33
36
  error: [error: unknown];
34
37
  typing: [typing: Typing];
35
38
  message: [message: Message];
36
- old_messages: [messages: Message[], type: ThreadType];
39
+ old_messages: [messages: Message[], type: ThreadType, hasMore?: boolean];
37
40
  seen_messages: [messages: SeenMessage[]];
38
41
  delivered_messages: [messages: DeliveredMessage[]];
42
+ unread_cleared: [items: ClearUnread[]];
39
43
  reaction: [reaction: Reaction];
40
44
  old_reactions: [reactions: Reaction[], isGroup: boolean];
41
45
  upload_attachment: [data: UploadEventData];
42
46
  undo: [data: Undo];
43
47
  friend_event: [data: FriendEvent];
44
48
  group_event: [data: GroupEvent];
49
+ sync_event: [data: SyncEvent];
50
+ call_event: [data: CallEvent];
45
51
  cipher_key: [key: string];
46
52
  ping: [];
47
53
  pong: [];
@@ -95,9 +101,11 @@ export declare class Listener extends EventEmitter<ListenerEvents> {
95
101
  stop(): void;
96
102
  sendWs(payload: WsPayload, requireId?: boolean): void;
97
103
  /**
98
- * Request old messages
104
+ * Request old messages from Zalo WebSocket queue (`cmd 510` for User, `cmd 511` for Group).
105
+ * When `lastMsgId` is `null` or omitted, defaults to `"10000000000000000"` to fetch the latest offline messages.
99
106
  *
100
- * @param lastMsgId
107
+ * @param threadType ThreadType.User or ThreadType.Group
108
+ * @param lastMsgId Optional cursor message ID (`actionId`) for pagination
101
109
  */
102
110
  requestOldMessages(threadType: ThreadType, lastMsgId?: string | null): void;
103
111
  /**
@@ -7,6 +7,9 @@ import { decodeEventData, getBrowserLanguage, getFriendEventType, getGroupEventT
7
7
  import { ZaloApiError } from "../Errors/ZaloApiError.js";
8
8
  import { GroupSeenMessage, UserSeenMessage } from "../models/SeenMessage.js";
9
9
  import { UserDeliveredMessage, GroupDeliveredMessage } from "../models/DeliveredMessage.js";
10
+ import { ClearUnread } from "../models/ClearUnread.js";
11
+ import { initializeSyncEvent } from "../models/SyncEvent.js";
12
+ import { initializeCallEvent } from "../models/CallEvent.js";
10
13
  export var CloseReason;
11
14
  (function (CloseReason) {
12
15
  CloseReason[CloseReason["ManualClosure"] = 1000] = "ManualClosure";
@@ -220,7 +223,7 @@ export class Listener extends EventEmitter {
220
223
  this.emit("error", event);
221
224
  };
222
225
  ws.onmessage = async (event) => {
223
- var _a, _b, _c;
226
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
224
227
  this.resetWatchdog();
225
228
  this.emit("pong"); // Pulse event to notify watchdog that socket is alive
226
229
  const { data } = event;
@@ -342,6 +345,21 @@ export class Listener extends EventEmitter {
342
345
  continue;
343
346
  this.emit("friend_event", friendEvent);
344
347
  }
348
+ else if (control.content.act_type == "syncmsgmb" ||
349
+ control.content.act_type == "transfer_msg") {
350
+ const syncEventData = typeof control.content.data == "string"
351
+ ? JSON.parse(control.content.data)
352
+ : control.content.data;
353
+ const syncEvent = initializeSyncEvent(syncEventData, control.content.act, control.content.act_type);
354
+ this.emit("sync_event", syncEvent);
355
+ }
356
+ else if (control.content.act_type == "voip") {
357
+ const callEventData = typeof control.content.data == "string"
358
+ ? JSON.parse(control.content.data)
359
+ : control.content.data;
360
+ const callEvent = initializeCallEvent(callEventData, control.content.act, control.content.act_type);
361
+ this.emit("call_event", callEvent);
362
+ }
345
363
  }
346
364
  }
347
365
  if (cmd == 612) {
@@ -369,17 +387,54 @@ export class Listener extends EventEmitter {
369
387
  const reactionObjects = reacts.map((react) => new Reaction(this.ctx.uid, react, isGroup));
370
388
  this.emit("old_reactions", reactionObjects, isGroup);
371
389
  }
372
- if (cmd == 510 && subCmd == 1) {
373
- const parsedData = (await decodeEventData(parsed, this.cipherKey)).data;
374
- const msgs = parsedData.msgs;
375
- const responseMsgs = msgs.map((msg) => new UserMessage(this.ctx.uid, msg));
376
- this.emit("old_messages", responseMsgs, ThreadType.User);
390
+ if (cmd == 510 && (subCmd == 0 || subCmd == 1)) {
391
+ const decoded = await decodeEventData(parsed, this.cipherKey);
392
+ const parsedData = (_d = decoded === null || decoded === void 0 ? void 0 : decoded.data) !== null && _d !== void 0 ? _d : decoded;
393
+ const msgs = ((_e = parsedData === null || parsedData === void 0 ? void 0 : parsedData.msgs) !== null && _e !== void 0 ? _e : []);
394
+ const responseMsgs = [];
395
+ for (const msg of msgs) {
396
+ if (typeof msg.content == "object" && hasOwn(msg.content, "deleteMsg")) {
397
+ const undoObject = new Undo(this.ctx.uid, msg, false);
398
+ if (undoObject.isSelf && !this.selfListen)
399
+ continue;
400
+ this.emit("undo", undoObject);
401
+ }
402
+ else {
403
+ responseMsgs.push(new UserMessage(this.ctx.uid, msg));
404
+ }
405
+ }
406
+ if (responseMsgs.length > 0 || msgs.length === 0) {
407
+ this.emit("old_messages", responseMsgs, ThreadType.User, (parsedData === null || parsedData === void 0 ? void 0 : parsedData.more) === 1);
408
+ }
377
409
  }
378
- if (cmd == 511 && subCmd == 1) {
379
- const parsedData = (await decodeEventData(parsed, this.cipherKey)).data;
380
- const groupMsgs = parsedData.groupMsgs;
381
- const responseMsgs = groupMsgs.map((msg) => new GroupMessage(this.ctx.uid, msg));
382
- this.emit("old_messages", responseMsgs, ThreadType.Group);
410
+ if (cmd == 511 && (subCmd == 0 || subCmd == 1)) {
411
+ const decoded = await decodeEventData(parsed, this.cipherKey);
412
+ const parsedData = (_f = decoded === null || decoded === void 0 ? void 0 : decoded.data) !== null && _f !== void 0 ? _f : decoded;
413
+ const groupMsgs = ((_g = parsedData === null || parsedData === void 0 ? void 0 : parsedData.groupMsgs) !== null && _g !== void 0 ? _g : []);
414
+ const responseMsgs = [];
415
+ for (const msg of groupMsgs) {
416
+ if (typeof msg.content == "object" && hasOwn(msg.content, "deleteMsg")) {
417
+ const undoObject = new Undo(this.ctx.uid, msg, true);
418
+ if (undoObject.isSelf && !this.selfListen)
419
+ continue;
420
+ this.emit("undo", undoObject);
421
+ }
422
+ else {
423
+ responseMsgs.push(new GroupMessage(this.ctx.uid, msg));
424
+ }
425
+ }
426
+ if (responseMsgs.length > 0 || groupMsgs.length === 0) {
427
+ this.emit("old_messages", responseMsgs, ThreadType.Group, (parsedData === null || parsedData === void 0 ? void 0 : parsedData.more) === 1);
428
+ }
429
+ }
430
+ if ((cmd == 504 || cmd == 524) && subCmd == 0) {
431
+ const decoded = await decodeEventData(parsed, this.cipherKey);
432
+ const parsedData = (_h = decoded === null || decoded === void 0 ? void 0 : decoded.data) !== null && _h !== void 0 ? _h : decoded;
433
+ const clearUnreads = ((_j = parsedData === null || parsedData === void 0 ? void 0 : parsedData.clearUnreads) !== null && _j !== void 0 ? _j : []);
434
+ if (Array.isArray(clearUnreads) && clearUnreads.length > 0) {
435
+ const items = clearUnreads.map((item) => new ClearUnread(item));
436
+ this.emit("unread_cleared", items);
437
+ }
383
438
  }
384
439
  if (cmd == 602 && subCmd == 0) {
385
440
  const parsedData = (await decodeEventData(parsed, this.cipherKey)).data;
@@ -475,16 +530,18 @@ export class Listener extends EventEmitter {
475
530
  }
476
531
  }
477
532
  /**
478
- * Request old messages
533
+ * Request old messages from Zalo WebSocket queue (`cmd 510` for User, `cmd 511` for Group).
534
+ * When `lastMsgId` is `null` or omitted, defaults to `"10000000000000000"` to fetch the latest offline messages.
479
535
  *
480
- * @param lastMsgId
536
+ * @param threadType ThreadType.User or ThreadType.Group
537
+ * @param lastMsgId Optional cursor message ID (`actionId`) for pagination
481
538
  */
482
539
  requestOldMessages(threadType, lastMsgId = null) {
483
540
  const payload = {
484
541
  version: 1,
485
542
  cmd: threadType === ThreadType.User ? 510 : 511,
486
543
  subCmd: 1,
487
- data: { first: true, lastId: lastMsgId, preIds: [] },
544
+ data: { first: true, lastId: lastMsgId !== null && lastMsgId !== void 0 ? lastMsgId : "10000000000000000", preIds: [] },
488
545
  };
489
546
  this.sendWs(payload);
490
547
  }