@softtynet/zca-js 2.1.16 → 2.1.18

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 CHANGED
@@ -18,6 +18,7 @@
18
18
  - [Basic Usages](#basic-usages)
19
19
  - [Login](#login)
20
20
  - [Listen for new messages](#listen-for-new-messages)
21
+ - [Synchronize Messages from Mobile](#synchronize-messages-from-mobile)
21
22
  - [Send a message](#send-a-message)
22
23
  - [Get/Send a sticker](#getsend-a-sticker)
23
24
  - [Example](#example)
@@ -114,6 +115,44 @@ api.listener.start();
114
115
  > [!IMPORTANT]
115
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.
116
117
 
118
+ ### Synchronize Messages from Mobile
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:
124
+
125
+ ```javascript
126
+ import { Zalo, LoginQRCallbackEventType } from "zca-js";
127
+
128
+ const zalo = new Zalo({ selfListen: true });
129
+ const api = await zalo.loginQR({ qrPath: "./qr.png" });
130
+
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!`);
134
+ });
135
+
136
+ api.listener.start();
137
+ ```
138
+
139
+ #### Case 2: Manual Trigger Sync (from existing session)
140
+ To request message synchronization from your phone at any time:
141
+
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!");
147
+ });
148
+
149
+ api.listener.on("old_messages", (messages, type) => {
150
+ console.log(`Received ${messages.length} synced messages from phone!`);
151
+ });
152
+
153
+ api.listener.start();
154
+ ```
155
+
117
156
  ### Send a message
118
157
 
119
158
  ```javascript
@@ -0,0 +1,4 @@
1
+ export type DecryptGidsResponse = {
2
+ gids: Record<string, string>;
3
+ };
4
+ export declare const decryptGidsFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (gids: string | string[]) => Promise<DecryptGidsResponse>;
@@ -0,0 +1,28 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const decryptGidsFactory = apiFactory()((api, ctx, utils) => {
4
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.profile[0]}/api/gid/decrypt`);
5
+ /**
6
+ * Giải mã danh sách Global ID (g_...) ẩn danh thành User ID thật
7
+ *
8
+ * @param gids Danh sách Global ID (hoặc 1 GID đơn lẻ)
9
+ * @throws {ZaloApiError}
10
+ */
11
+ return async function decryptGids(gids) {
12
+ if (!gids)
13
+ throw new ZaloApiError("Missing gids");
14
+ if (!Array.isArray(gids))
15
+ gids = [gids];
16
+ const params = {
17
+ gids,
18
+ imei: ctx.imei,
19
+ };
20
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
21
+ if (!encryptedParams)
22
+ throw new ZaloApiError("Failed to encrypt params");
23
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
24
+ method: "GET",
25
+ });
26
+ return utils.resolve(response);
27
+ };
28
+ });
@@ -0,0 +1,6 @@
1
+ export type DownloadTransferSyncOptions = string | {
2
+ key: string;
3
+ exp: number | string;
4
+ sig: string;
5
+ };
6
+ export declare const downloadTransferSyncFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (options: DownloadTransferSyncOptions) => Promise<Buffer>;
@@ -0,0 +1,30 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const downloadTransferSyncFactory = apiFactory()((api, ctx, utils) => {
4
+ /**
5
+ * Tải khối dữ liệu snapshot đồng bộ tin nhắn từ máy chủ transfersync.zaloapp.com
6
+ *
7
+ * @param options URL đầy đủ hoặc object chứa { key, exp, sig }
8
+ * @throws {ZaloApiError}
9
+ */
10
+ return async function downloadTransferSync(options) {
11
+ let downloadUrl = "";
12
+ if (typeof options === "string") {
13
+ downloadUrl = options;
14
+ }
15
+ else {
16
+ if (!options.key || !options.exp || !options.sig) {
17
+ throw new ZaloApiError("Missing key, exp or sig in options");
18
+ }
19
+ downloadUrl = `https://transfersync.zaloapp.com/api/transfer-sync-v2/download?key=${options.key}&exp=${options.exp}&sig=${options.sig}`;
20
+ }
21
+ const response = await utils.request(downloadUrl, {
22
+ method: "GET",
23
+ });
24
+ if (!response.ok) {
25
+ throw new ZaloApiError(`Failed to download transfer snapshot: ${response.statusText} (${response.status})`);
26
+ }
27
+ const arrayBuffer = await response.arrayBuffer();
28
+ return Buffer.from(arrayBuffer);
29
+ };
30
+ });
@@ -1,8 +1,18 @@
1
1
  import { GroupMessage } from "../models/index.js";
2
2
  export type GetGroupChatHistoryResponse = {
3
- lastActionId: string;
4
- lastActionIdOther: string;
5
- more: number;
3
+ error?: number;
4
+ lastMsgId?: number;
5
+ minMsgId?: number;
6
+ maxMsgId?: number;
7
+ msgJumpId?: number;
8
+ hasMore?: boolean;
9
+ isOld?: boolean;
10
+ isFiltered?: boolean;
11
+ rootMsgId?: number;
12
+ isRootDel?: boolean;
6
13
  groupMsgs: GroupMessage[];
14
+ tsJoinGroup?: number;
15
+ isFilteredByPhase?: boolean;
16
+ isFilteredByTimeJoin?: boolean;
7
17
  };
8
18
  export declare const getGroupChatHistoryFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (groupId: string, count?: number) => Promise<GetGroupChatHistoryResponse>;
@@ -2,35 +2,63 @@ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
2
  import { apiFactory } from "../utils.js";
3
3
  import { GroupMessage } from "../models/index.js";
4
4
  export const getGroupChatHistoryFactory = apiFactory()((api, ctx, utils) => {
5
- const serviceURL = utils.makeURL(`${api.zpwServiceMap.group[0]}/api/group/history`);
5
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.group_cloud_message[0]}/api/cm/getrecentv2`);
6
6
  /**
7
- * Get group chat history
7
+ * Get group chat history from Zalo Cloud Message (newest first)
8
8
  *
9
9
  * @param groupId group id
10
- * @param count count of messages to return (default: 50)
10
+ * @param count count of messages to return (default: 50, max safe limit: 200)
11
11
  *
12
12
  * @throws {ZaloApiError}
13
13
  */
14
14
  return async function getGroupChatHistory(groupId, count = 50) {
15
- const params = {
16
- grid: groupId,
17
- count: count,
18
- };
19
- const encryptedParams = utils.encodeAES(JSON.stringify(params));
20
- if (!encryptedParams)
21
- throw new ZaloApiError("Failed to encrypt params");
22
- const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
23
- method: "GET",
24
- });
25
- return utils.resolve(response, (result) => {
26
- let data = result.data;
27
- if (typeof data === "string") {
28
- data = JSON.parse(data);
15
+ if (!groupId)
16
+ throw new ZaloApiError("Missing groupId");
17
+ if (groupId.startsWith("g"))
18
+ groupId = groupId.slice(1);
19
+ const safeCount = Math.min(Math.max(count, 1), 200);
20
+ const messages = [];
21
+ let cursor = 0;
22
+ let lastPage = null;
23
+ let pageCount = 0;
24
+ while (messages.length < safeCount && pageCount < 10) {
25
+ pageCount++;
26
+ const params = {
27
+ groupId,
28
+ globalMsgId: cursor,
29
+ count: Math.min(50, safeCount - messages.length),
30
+ msgIds: [],
31
+ imei: ctx.imei,
32
+ src: 3,
33
+ };
34
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
35
+ if (!encryptedParams)
36
+ throw new ZaloApiError("Failed to encrypt params");
37
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams, nretry: 0 }), {
38
+ method: "GET",
39
+ });
40
+ lastPage = await utils.resolve(response, (result) => {
41
+ let data = result.data;
42
+ if (typeof data === "string") {
43
+ data = JSON.parse(data);
44
+ }
45
+ return data;
46
+ });
47
+ if (!lastPage || !Array.isArray(lastPage.groupMsgs))
48
+ break;
49
+ for (const rawMsg of lastPage.groupMsgs) {
50
+ if (messages.length >= safeCount)
51
+ break;
52
+ if (messages.some((m) => m.msgId === rawMsg.msgId))
53
+ continue;
54
+ messages.push(rawMsg);
29
55
  }
30
- for (let i = 0; i < data.groupMsgs.length; i++) {
31
- data.groupMsgs[i] = new GroupMessage(ctx.uid, data.groupMsgs[i]);
32
- }
33
- return data;
34
- });
56
+ const nextCursor = Number(lastPage.lastMsgId);
57
+ if (!lastPage.hasMore || !nextCursor || nextCursor === cursor)
58
+ break;
59
+ cursor = nextCursor;
60
+ }
61
+ const groupMsgs = messages.map((data) => new GroupMessage(ctx.uid, data));
62
+ return Object.assign({}, lastPage || {}, { groupMsgs });
35
63
  };
36
64
  });
@@ -28,6 +28,7 @@ export declare enum CloseReason {
28
28
  interface ListenerEvents {
29
29
  connected: [];
30
30
  disconnected: [code: CloseReason, reason: string];
31
+ reconnecting: [code: CloseReason];
31
32
  closed: [code: CloseReason, reason: string];
32
33
  error: [error: unknown];
33
34
  typing: [typing: Typing];
@@ -62,6 +63,8 @@ export declare class Listener extends EventEmitter<ListenerEvents> {
62
63
  private selfListen;
63
64
  private pingInterval?;
64
65
  private retryTimeout?;
66
+ private watchdogTimeout?;
67
+ private consecutive1006Count;
65
68
  private id;
66
69
  constructor(ctx: ContextSession, urls: string[]);
67
70
  /**
@@ -80,7 +83,10 @@ export declare class Listener extends EventEmitter<ListenerEvents> {
80
83
  * @deprecated Use `on` method instead
81
84
  */
82
85
  onMessage(cb: OnMessageCallback): void;
86
+ private isRetryable;
83
87
  private canRetry;
88
+ private resetRetryCount;
89
+ private resetWatchdog;
84
90
  private shouldRotate;
85
91
  private rotateEndpoint;
86
92
  start({ retryOnClose }?: {
@@ -3,7 +3,7 @@ import WebSocket from "ws";
3
3
  import { initializeFriendEvent } from "../models/FriendEvent.js";
4
4
  import { initializeGroupEvent } from "../models/GroupEvent.js";
5
5
  import { GroupMessage, UserMessage, Reaction, Undo, ThreadType, GroupTyping, UserTyping } from "../models/index.js";
6
- import { decodeEventData, getFriendEventType, getGroupEventType, hasOwn, logger, makeURL } from "../utils.js";
6
+ import { decodeEventData, getBrowserLanguage, getFriendEventType, getGroupEventType, hasOwn, logger, makeURL } from "../utils.js";
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";
@@ -19,6 +19,7 @@ export class Listener extends EventEmitter {
19
19
  super();
20
20
  this.ctx = ctx;
21
21
  this.urls = urls;
22
+ this.consecutive1006Count = 0;
22
23
  this.id = 0;
23
24
  if (!ctx.cookie)
24
25
  throw new ZaloApiError("Cookie is not available");
@@ -70,26 +71,71 @@ export class Listener extends EventEmitter {
70
71
  onMessage(cb) {
71
72
  this.onMessageCallback = cb;
72
73
  }
74
+ isRetryable(code) {
75
+ return (code === CloseReason.AbnormalClosure ||
76
+ this.ctx.settings.features.socket.close_and_retry_codes.includes(code));
77
+ }
73
78
  canRetry(code) {
74
- if (!this.ctx.settings.features.socket.close_and_retry_codes.includes(code))
79
+ var _a;
80
+ if (!this.isRetryable(code))
75
81
  return false;
76
- if (this.retryCount[code.toString()].count >= this.retryCount[code.toString()].max)
82
+ const retry = (_a = this.retryCount[code.toString()]) !== null && _a !== void 0 ? _a : this.retryCount["internal"];
83
+ if (!retry || retry.count >= retry.max)
77
84
  return false;
78
- this.retryCount[code.toString()].count++;
79
- const { count, max, times } = this.retryCount[code.toString()];
80
- const retryTime = count - 1 < times.length ? times[count - 1] : times[times.length - 1];
85
+ retry.count++;
86
+ const { count, max, times } = retry;
87
+ let retryTime;
88
+ if (count - 1 < times.length) {
89
+ retryTime = times[count - 1];
90
+ }
91
+ else {
92
+ const lastTime = times[times.length - 1] || 1000;
93
+ const multiplier = Math.min(Math.pow(1.5, count - times.length), 10);
94
+ const jitter = Math.floor(Math.random() * 1000);
95
+ retryTime = Math.min(Math.floor(lastTime * multiplier) + jitter, 30000);
96
+ }
81
97
  logger(this.ctx).verbose(`Retry for code ${code} in ${retryTime}ms (${count}/${max})`);
82
98
  return retryTime;
83
99
  }
100
+ resetRetryCount() {
101
+ for (const retry of Object.values(this.retryCount)) {
102
+ retry.count = 0;
103
+ }
104
+ }
105
+ resetWatchdog() {
106
+ if (this.watchdogTimeout)
107
+ clearTimeout(this.watchdogTimeout);
108
+ this.watchdogTimeout = setTimeout(() => {
109
+ logger(this.ctx).verbose("Watchdog timeout: no packet/pong received from Zalo WebSocket in 50s. Forcing reconnect...");
110
+ if (this.ws) {
111
+ try {
112
+ if (typeof this.ws.terminate === "function") {
113
+ this.ws.terminate();
114
+ }
115
+ else if (typeof this.ws.close === "function") {
116
+ this.ws.close(CloseReason.AbnormalClosure);
117
+ }
118
+ }
119
+ catch (_a) {
120
+ // Ignore if already terminated
121
+ }
122
+ }
123
+ }, 50000);
124
+ }
84
125
  shouldRotate(code) {
85
- if (!this.ctx.settings.features.socket.rotate_error_codes.includes(code))
86
- return false;
87
- if (this.rotateCount >= this.urls.length - 1)
126
+ if (code === CloseReason.AbnormalClosure) {
127
+ this.consecutive1006Count++;
128
+ if (this.consecutive1006Count >= 2) {
129
+ this.consecutive1006Count = 0;
130
+ return true;
131
+ }
88
132
  return false;
89
- return true;
133
+ }
134
+ this.consecutive1006Count = 0;
135
+ return this.ctx.settings.features.socket.rotate_error_codes.includes(code);
90
136
  }
91
137
  rotateEndpoint() {
92
- this.rotateCount++;
138
+ this.rotateCount = (this.rotateCount + 1) % this.urls.length;
93
139
  this.wsURL = makeURL(this.ctx, this.urls[this.rotateCount], {
94
140
  t: Date.now(),
95
141
  });
@@ -98,37 +144,46 @@ export class Listener extends EventEmitter {
98
144
  start({ retryOnClose = false } = {}) {
99
145
  if (this.ws)
100
146
  throw new ZaloApiError("Already started");
147
+ this.wsURL = makeURL(this.ctx, this.urls[this.rotateCount], {
148
+ t: Date.now(),
149
+ });
150
+ const cookie = this.ctx.cookie.getCookieStringSync("https://chat.zalo.me");
101
151
  const ws = new WebSocket(this.wsURL, {
102
152
  headers: {
103
- "accept-encoding": "gzip, deflate, br, zstd",
104
- "accept-language": "en-US,en;q=0.9",
153
+ "accept-language": getBrowserLanguage(this.ctx.language),
105
154
  "cache-control": "no-cache",
106
155
  connection: "Upgrade",
107
156
  host: new URL(this.wsURL).host,
108
157
  origin: "https://chat.zalo.me",
109
- prgama: "no-cache",
158
+ pragma: "no-cache",
110
159
  "sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
111
160
  "sec-websocket-version": "13",
112
161
  upgrade: "websocket",
113
162
  "user-agent": this.userAgent,
114
- cookie: this.cookie,
163
+ cookie,
115
164
  },
116
- agent: this.ctx.options.agent
165
+ agent: this.ctx.options.agent,
117
166
  });
118
167
  this.ws = ws;
119
168
  ws.onopen = () => {
169
+ this.consecutive1006Count = 0;
170
+ this.resetRetryCount();
171
+ this.resetWatchdog();
120
172
  this.onConnectedCallback();
121
173
  this.emit("connected");
122
174
  };
123
175
  ws.onclose = (event) => {
124
176
  this.reset();
125
177
  this.emit("disconnected", event.code, event.reason);
126
- const retry = retryOnClose && this.canRetry(event.code);
178
+ const retry = retryOnClose &&
179
+ this.isRetryable(event.code) &&
180
+ this.canRetry(event.code);
127
181
  if (retry && retryOnClose) {
128
182
  const shouldRotate = this.shouldRotate(event.code);
129
183
  if (shouldRotate) {
130
184
  this.rotateEndpoint();
131
185
  }
186
+ this.emit("reconnecting", event.code);
132
187
  this.retryTimeout = setTimeout(() => {
133
188
  this.start({ retryOnClose: true });
134
189
  }, retry);
@@ -143,6 +198,8 @@ export class Listener extends EventEmitter {
143
198
  this.emit("error", event);
144
199
  };
145
200
  ws.onmessage = async (event) => {
201
+ var _a, _b, _c;
202
+ this.resetWatchdog();
146
203
  this.emit("pong"); // Pulse event to notify watchdog that socket is alive
147
204
  const { data } = event;
148
205
  if (!(data instanceof Buffer))
@@ -170,9 +227,10 @@ export class Listener extends EventEmitter {
170
227
  this.sendWs(payload, false);
171
228
  this.emit("ping");
172
229
  };
230
+ const pingIntervalTime = Math.min(Math.max(((_c = (_b = (_a = this.ctx.settings) === null || _a === void 0 ? void 0 : _a.features) === null || _b === void 0 ? void 0 : _b.socket) === null || _c === void 0 ? void 0 : _c.ping_interval) || 25000, 10000), 25000);
173
231
  this.pingInterval = setInterval(() => {
174
232
  ping();
175
- }, this.ctx.settings.features.socket.ping_interval);
233
+ }, pingIntervalTime);
176
234
  }
177
235
  if (version == 1 && cmd == 501 && subCmd == 0) {
178
236
  const parsedData = (await decodeEventData(parsed, this.cipherKey)).data;
@@ -369,6 +427,8 @@ export class Listener extends EventEmitter {
369
427
  stop() {
370
428
  if (this.retryTimeout)
371
429
  clearTimeout(this.retryTimeout);
430
+ if (this.watchdogTimeout)
431
+ clearTimeout(this.watchdogTimeout);
372
432
  if (this.ws) {
373
433
  if (this.ws.readyState !== WebSocket.CLOSED) {
374
434
  this.ws.close(CloseReason.ManualClosure);
@@ -425,6 +485,8 @@ export class Listener extends EventEmitter {
425
485
  this.cipherKey = undefined;
426
486
  if (this.pingInterval)
427
487
  clearInterval(this.pingInterval);
488
+ if (this.watchdogTimeout)
489
+ clearTimeout(this.watchdogTimeout);
428
490
  }
429
491
  }
430
492
  function getHeader(buffer) {
@@ -3,7 +3,7 @@ import { decryptResp, getSignKey, logger, makeURL, ParamsEncryptor, request } fr
3
3
  export async function login(ctx, encryptParams) {
4
4
  const encryptedParams = await getEncryptParam(ctx, encryptParams, "getlogininfo");
5
5
  try {
6
- const response = await request(ctx, makeURL(ctx, "https://wpa.chat.zalo.me/api/login/getLoginInfo", Object.assign(Object.assign({}, encryptedParams.params), { nretry: 0 })));
6
+ const response = await request(ctx, makeURL(ctx, "https://wpa.chat.zalo.me/api/login/getLoginInfo", Object.assign(Object.assign({}, encryptedParams.params), { nretry: 0 }), false));
7
7
  if (!response.ok)
8
8
  throw new ZaloApiError("Failed to fetch login info: " + response.statusText);
9
9
  const data = await response.json();
@@ -1,35 +1,20 @@
1
1
  import { CookieJar } from "tough-cookie";
2
2
  import { writeFile } from "node:fs/promises";
3
3
  import { ZaloApiError } from "../Errors/ZaloApiError.js";
4
- import { logger, request, getPlatformFromUA, getChromeVersionFromUA } from "../utils.js";
4
+ import { logger, request, getBrowserLanguage, getSecChUaHeaders } from "../utils.js";
5
5
  import { ZaloApiLoginQRAborted } from "../Errors/ZaloApiLoginQRAborted.js";
6
6
  import { ZaloApiLoginQRDeclined } from "../Errors/ZaloApiLoginQRDeclined.js";
7
7
  /**
8
- * Tạo bộ header Sec-CH-UA dùng chung cho toàn bộ các request trong luồng đăng nhập QR.
8
+ * Tạo bộ header Sec-CH-UA và ngôn ngữ dùng chung cho toàn bộ các request trong luồng đăng nhập QR.
9
9
  *
10
- * Trước đây 8 hàm con trong loginQR đều copy-paste cùng một bộ header với giá trị
11
- * `sec-ch-ua-platform: "Windows"` bị hardcode cứng. Điều này gây ra mâu thuẫn fingerprint
12
- * (Browser Mismatch) khi người dùng truyền vào User-Agent của Mac hoặc Linux,
13
- * khiến hệ thống chống bot của Zalo phát hiện và ban session ngay lập tức.
10
+ * Tất cả giá trị sec-ch-ua-* đều được suy luận ĐỘNG từ ctx.userAgent (chỉ khi là Chromium)
11
+ * và accept-language theo ctx.language, đảm bảo nhất quán 100% với môi trường trình duyệt thật.
14
12
  *
15
- * Giải pháp: Tất cả giá trị sec-ch-ua-* đều được suy luận ĐỘNG từ ctx.userAgent,
16
- * đảm bảo nhất quán 100% giữa User-Agent và các Client Hint headers.
17
- *
18
- * @param ctx - Context chứa ctx.userAgent đã được gán trước khi gọi hàm này
13
+ * @param ctx - Context chứa ctx.userAgent và ctx.language
19
14
  * @param extra - Các header bổ sung đặc thù cho từng request (sec-fetch-dest, Referer...)
20
15
  */
21
16
  function getLoginHeaders(ctx, extra) {
22
- // Suy luận platform từ UA: "Windows" | "macOS" | "Linux" (mặc định "Windows" nếu không nhận ra)
23
- const platform = getPlatformFromUA(ctx.userAgent);
24
- // Trích xuất phiên bản Chrome major từ UA (ví dụ: "130"), fallback về "130" nếu không phải Chrome
25
- const chromeVer = getChromeVersionFromUA(ctx.userAgent);
26
- return Object.assign({ "accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5",
27
- // sec-ch-ua phải khớp với phiên bản Chrome trong User-Agent
28
- "sec-ch-ua": `"Chromium";v="${chromeVer}", "Google Chrome";v="${chromeVer}", "Not?A_Brand";v="99"`,
29
- // Luôn là desktop (?0) vì Zalo Web không hỗ trợ mobile
30
- "sec-ch-ua-mobile": "?0",
31
- // sec-ch-ua-platform PHẢI nhất quán với User-Agent — đây là nguyên nhân gốc rễ gây ban session
32
- "sec-ch-ua-platform": `"${platform}"` }, extra);
17
+ return Object.assign(Object.assign({ "accept-language": getBrowserLanguage(ctx.language) }, getSecChUaHeaders(ctx.userAgent)), extra);
33
18
  }
34
19
  export var LoginQRCallbackEventType;
35
20
  (function (LoginQRCallbackEventType) {
@@ -0,0 +1,10 @@
1
+ export type PreloadConversationsResponse = {
2
+ conversations?: Array<{
3
+ conv_id: string;
4
+ last_msgs?: unknown[];
5
+ unread_count?: number;
6
+ [key: string]: unknown;
7
+ }>;
8
+ [key: string]: unknown;
9
+ };
10
+ export declare const preloadConversationsFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => () => Promise<PreloadConversationsResponse>;
@@ -0,0 +1,23 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const preloadConversationsFactory = apiFactory()((api, ctx, utils) => {
4
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.conversation[0]}/api/preloadconvers/get-last-msgs`);
5
+ /**
6
+ * Preload last messages across all active conversations.
7
+ * Useful for initial state loading and catching up missed messages after reconnect.
8
+ *
9
+ * @throws {ZaloApiError}
10
+ */
11
+ return async function preloadConversations() {
12
+ const params = {
13
+ imei: ctx.imei,
14
+ };
15
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
16
+ if (!encryptedParams)
17
+ throw new ZaloApiError("Failed to encrypt params");
18
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
19
+ method: "GET",
20
+ });
21
+ return utils.resolve(response);
22
+ };
23
+ });
@@ -0,0 +1,4 @@
1
+ export type RequestSyncFromPhoneResponse = {
2
+ success: boolean;
3
+ };
4
+ export declare const requestSyncFromPhoneFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => () => Promise<RequestSyncFromPhoneResponse>;
@@ -0,0 +1,27 @@
1
+ import { ZaloApiError } from "../Errors/ZaloApiError.js";
2
+ import { apiFactory } from "../utils.js";
3
+ export const requestSyncFromPhoneFactory = apiFactory()((api, ctx) => {
4
+ /**
5
+ * Gửi tín hiệu kích hoạt yêu cầu đồng bộ tin nhắn từ xa sang app điện thoại (Trường hợp 2)
6
+ *
7
+ * @throws {ZaloApiError}
8
+ */
9
+ return async function requestSyncFromPhone() {
10
+ if (!api.listener) {
11
+ throw new ZaloApiError("WebSocket listener is not initialized. Please call api.listener.start() first.");
12
+ }
13
+ // Gửi frame yêu cầu đồng bộ qua WebSocket
14
+ const syncPayload = {
15
+ version: 1,
16
+ cmd: 510,
17
+ subCmd: 0,
18
+ data: {
19
+ act: "sync_request",
20
+ imei: ctx.imei,
21
+ ts: Date.now(),
22
+ },
23
+ };
24
+ api.listener.sendWs(syncPayload, true);
25
+ return { success: true };
26
+ };
27
+ });
package/dist/apis.d.ts CHANGED
@@ -21,6 +21,7 @@ import { createNoteFactory } from "./apis/createNote.js";
21
21
  import { createPollFactory } from "./apis/createPoll.js";
22
22
  import { createProductCatalogFactory } from "./apis/createProductCatalog.js";
23
23
  import { createReminderFactory } from "./apis/createReminder.js";
24
+ import { decryptGidsFactory } from "./apis/decryptGids.js";
24
25
  import { deleteAutoReplyFactory } from "./apis/deleteAutoReply.js";
25
26
  import { deleteAvatarFactory } from "./apis/deleteAvatar.js";
26
27
  import { deleteCatalogFactory } from "./apis/deleteCatalog.js";
@@ -30,6 +31,7 @@ import { deleteMessageFactory } from "./apis/deleteMessage.js";
30
31
  import { deleteProductCatalogFactory } from "./apis/deleteProductCatalog.js";
31
32
  import { disableGroupLinkFactory } from "./apis/disableGroupLink.js";
32
33
  import { disperseGroupFactory } from "./apis/disperseGroup.js";
34
+ import { downloadTransferSyncFactory } from "./apis/downloadTransferSync.js";
33
35
  import { editNoteFactory } from "./apis/editNote.js";
34
36
  import { editReminderFactory } from "./apis/editReminder.js";
35
37
  import { enableGroupLinkFactory } from "./apis/enableGroupLink.js";
@@ -94,6 +96,7 @@ import { lastOnlineFactory } from "./apis/lastOnline.js";
94
96
  import { leaveGroupFactory } from "./apis/leaveGroup.js";
95
97
  import { lockPollFactory } from "./apis/lockPoll.js";
96
98
  import { parseLinkFactory } from "./apis/parseLink.js";
99
+ import { preloadConversationsFactory } from "./apis/preloadConversations.js";
97
100
  import { rejectFriendRequestFactory } from "./apis/rejectFriendRequest.js";
98
101
  import { removeFriendFactory } from "./apis/removeFriend.js";
99
102
  import { removeFriendAliasFactory } from "./apis/removeFriendAlias.js";
@@ -103,6 +106,7 @@ import { removeQuickMessageFactory } from "./apis/removeQuickMessage.js";
103
106
  import { removeReminderFactory } from "./apis/removeReminder.js";
104
107
  import { removeUnreadMarkFactory } from "./apis/removeUnreadMark.js";
105
108
  import { removeUserFromGroupFactory } from "./apis/removeUserFromGroup.js";
109
+ import { requestSyncFromPhoneFactory } from "./apis/requestSyncFromPhone.js";
106
110
  import { resetHiddenConversPinFactory } from "./apis/resetHiddenConversPin.js";
107
111
  import { reuseAvatarFactory } from "./apis/reuseAvatar.js";
108
112
  import { reviewPendingMemberRequestFactory } from "./apis/reviewPendingMemberRequest.js";
@@ -171,6 +175,7 @@ export declare class API {
171
175
  createPoll: ReturnType<typeof createPollFactory>;
172
176
  createProductCatalog: ReturnType<typeof createProductCatalogFactory>;
173
177
  createReminder: ReturnType<typeof createReminderFactory>;
178
+ decryptGids: ReturnType<typeof decryptGidsFactory>;
174
179
  deleteAutoReply: ReturnType<typeof deleteAutoReplyFactory>;
175
180
  deleteAvatar: ReturnType<typeof deleteAvatarFactory>;
176
181
  deleteCatalog: ReturnType<typeof deleteCatalogFactory>;
@@ -180,6 +185,7 @@ export declare class API {
180
185
  deleteProductCatalog: ReturnType<typeof deleteProductCatalogFactory>;
181
186
  disableGroupLink: ReturnType<typeof disableGroupLinkFactory>;
182
187
  disperseGroup: ReturnType<typeof disperseGroupFactory>;
188
+ downloadTransferSync: ReturnType<typeof downloadTransferSyncFactory>;
183
189
  editNote: ReturnType<typeof editNoteFactory>;
184
190
  editReminder: ReturnType<typeof editReminderFactory>;
185
191
  enableGroupLink: ReturnType<typeof enableGroupLinkFactory>;
@@ -244,6 +250,7 @@ export declare class API {
244
250
  leaveGroup: ReturnType<typeof leaveGroupFactory>;
245
251
  lockPoll: ReturnType<typeof lockPollFactory>;
246
252
  parseLink: ReturnType<typeof parseLinkFactory>;
253
+ preloadConversations: ReturnType<typeof preloadConversationsFactory>;
247
254
  rejectFriendRequest: ReturnType<typeof rejectFriendRequestFactory>;
248
255
  removeFriend: ReturnType<typeof removeFriendFactory>;
249
256
  removeFriendAlias: ReturnType<typeof removeFriendAliasFactory>;
@@ -253,6 +260,7 @@ export declare class API {
253
260
  removeReminder: ReturnType<typeof removeReminderFactory>;
254
261
  removeUnreadMark: ReturnType<typeof removeUnreadMarkFactory>;
255
262
  removeUserFromGroup: ReturnType<typeof removeUserFromGroupFactory>;
263
+ requestSyncFromPhone: ReturnType<typeof requestSyncFromPhoneFactory>;
256
264
  resetHiddenConversPin: ReturnType<typeof resetHiddenConversPinFactory>;
257
265
  reuseAvatar: ReturnType<typeof reuseAvatarFactory>;
258
266
  reviewPendingMemberRequest: ReturnType<typeof reviewPendingMemberRequestFactory>;