@softtynet/zca-js 2.1.16 → 2.1.17

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.
@@ -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
+ });
package/dist/apis.d.ts CHANGED
@@ -94,6 +94,7 @@ import { lastOnlineFactory } from "./apis/lastOnline.js";
94
94
  import { leaveGroupFactory } from "./apis/leaveGroup.js";
95
95
  import { lockPollFactory } from "./apis/lockPoll.js";
96
96
  import { parseLinkFactory } from "./apis/parseLink.js";
97
+ import { preloadConversationsFactory } from "./apis/preloadConversations.js";
97
98
  import { rejectFriendRequestFactory } from "./apis/rejectFriendRequest.js";
98
99
  import { removeFriendFactory } from "./apis/removeFriend.js";
99
100
  import { removeFriendAliasFactory } from "./apis/removeFriendAlias.js";
@@ -244,6 +245,7 @@ export declare class API {
244
245
  leaveGroup: ReturnType<typeof leaveGroupFactory>;
245
246
  lockPoll: ReturnType<typeof lockPollFactory>;
246
247
  parseLink: ReturnType<typeof parseLinkFactory>;
248
+ preloadConversations: ReturnType<typeof preloadConversationsFactory>;
247
249
  rejectFriendRequest: ReturnType<typeof rejectFriendRequestFactory>;
248
250
  removeFriend: ReturnType<typeof removeFriendFactory>;
249
251
  removeFriendAlias: ReturnType<typeof removeFriendAliasFactory>;
package/dist/apis.js CHANGED
@@ -94,6 +94,7 @@ import { lastOnlineFactory } from "./apis/lastOnline.js";
94
94
  import { leaveGroupFactory } from "./apis/leaveGroup.js";
95
95
  import { lockPollFactory } from "./apis/lockPoll.js";
96
96
  import { parseLinkFactory } from "./apis/parseLink.js";
97
+ import { preloadConversationsFactory } from "./apis/preloadConversations.js";
97
98
  import { rejectFriendRequestFactory } from "./apis/rejectFriendRequest.js";
98
99
  import { removeFriendFactory } from "./apis/removeFriend.js";
99
100
  import { removeFriendAliasFactory } from "./apis/removeFriendAlias.js";
@@ -244,6 +245,7 @@ export class API {
244
245
  this.leaveGroup = leaveGroupFactory(ctx, this);
245
246
  this.lockPoll = lockPollFactory(ctx, this);
246
247
  this.parseLink = parseLinkFactory(ctx, this);
248
+ this.preloadConversations = preloadConversationsFactory(ctx, this);
247
249
  this.rejectFriendRequest = rejectFriendRequestFactory(ctx, this);
248
250
  this.removeFriend = removeFriendFactory(ctx, this);
249
251
  this.removeFriendAlias = removeFriendAliasFactory(ctx, this);
@@ -14,36 +14,64 @@ require('../models/Reminder.cjs');
14
14
  require('../models/ZBusiness.cjs');
15
15
 
16
16
  const getGroupChatHistoryFactory = utils.apiFactory()((api, ctx, utils) => {
17
- const serviceURL = utils.makeURL(`${api.zpwServiceMap.group[0]}/api/group/history`);
17
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.group_cloud_message[0]}/api/cm/getrecentv2`);
18
18
  /**
19
- * Get group chat history
19
+ * Get group chat history from Zalo Cloud Message (newest first)
20
20
  *
21
21
  * @param groupId group id
22
- * @param count count of messages to return (default: 50)
22
+ * @param count count of messages to return (default: 50, max safe limit: 200)
23
23
  *
24
24
  * @throws {ZaloApiError}
25
25
  */
26
26
  return async function getGroupChatHistory(groupId, count = 50) {
27
- const params = {
28
- grid: groupId,
29
- count: count,
30
- };
31
- const encryptedParams = utils.encodeAES(JSON.stringify(params));
32
- if (!encryptedParams)
33
- throw new ZaloApiError.ZaloApiError("Failed to encrypt params");
34
- const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
35
- method: "GET",
36
- });
37
- return utils.resolve(response, (result) => {
38
- let data = result.data;
39
- if (typeof data === "string") {
40
- data = JSON.parse(data);
27
+ if (!groupId)
28
+ throw new ZaloApiError.ZaloApiError("Missing groupId");
29
+ if (groupId.startsWith("g"))
30
+ groupId = groupId.slice(1);
31
+ const safeCount = Math.min(Math.max(count, 1), 200);
32
+ const messages = [];
33
+ let cursor = 0;
34
+ let lastPage = null;
35
+ let pageCount = 0;
36
+ while (messages.length < safeCount && pageCount < 10) {
37
+ pageCount++;
38
+ const params = {
39
+ groupId,
40
+ globalMsgId: cursor,
41
+ count: Math.min(50, safeCount - messages.length),
42
+ msgIds: [],
43
+ imei: ctx.imei,
44
+ src: 3,
45
+ };
46
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
47
+ if (!encryptedParams)
48
+ throw new ZaloApiError.ZaloApiError("Failed to encrypt params");
49
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams, nretry: 0 }), {
50
+ method: "GET",
51
+ });
52
+ lastPage = await utils.resolve(response, (result) => {
53
+ let data = result.data;
54
+ if (typeof data === "string") {
55
+ data = JSON.parse(data);
56
+ }
57
+ return data;
58
+ });
59
+ if (!lastPage || !Array.isArray(lastPage.groupMsgs))
60
+ break;
61
+ for (const rawMsg of lastPage.groupMsgs) {
62
+ if (messages.length >= safeCount)
63
+ break;
64
+ if (messages.some((m) => m.msgId === rawMsg.msgId))
65
+ continue;
66
+ messages.push(rawMsg);
41
67
  }
42
- for (let i = 0; i < data.groupMsgs.length; i++) {
43
- data.groupMsgs[i] = new Message.GroupMessage(ctx.uid, data.groupMsgs[i]);
44
- }
45
- return data;
46
- });
68
+ const nextCursor = Number(lastPage.lastMsgId);
69
+ if (!lastPage.hasMore || !nextCursor || nextCursor === cursor)
70
+ break;
71
+ cursor = nextCursor;
72
+ }
73
+ const groupMsgs = messages.map((data) => new Message.GroupMessage(ctx.uid, data));
74
+ return Object.assign({}, lastPage || {}, { groupMsgs });
47
75
  };
48
76
  });
49
77
 
@@ -31,6 +31,7 @@ class Listener extends EventEmitter {
31
31
  super();
32
32
  this.ctx = ctx;
33
33
  this.urls = urls;
34
+ this.consecutive1006Count = 0;
34
35
  this.id = 0;
35
36
  if (!ctx.cookie)
36
37
  throw new ZaloApiError.ZaloApiError("Cookie is not available");
@@ -82,26 +83,71 @@ class Listener extends EventEmitter {
82
83
  onMessage(cb) {
83
84
  this.onMessageCallback = cb;
84
85
  }
86
+ isRetryable(code) {
87
+ return (code === exports.CloseReason.AbnormalClosure ||
88
+ this.ctx.settings.features.socket.close_and_retry_codes.includes(code));
89
+ }
85
90
  canRetry(code) {
86
- if (!this.ctx.settings.features.socket.close_and_retry_codes.includes(code))
91
+ var _a;
92
+ if (!this.isRetryable(code))
87
93
  return false;
88
- if (this.retryCount[code.toString()].count >= this.retryCount[code.toString()].max)
94
+ const retry = (_a = this.retryCount[code.toString()]) !== null && _a !== void 0 ? _a : this.retryCount["internal"];
95
+ if (!retry || retry.count >= retry.max)
89
96
  return false;
90
- this.retryCount[code.toString()].count++;
91
- const { count, max, times } = this.retryCount[code.toString()];
92
- const retryTime = count - 1 < times.length ? times[count - 1] : times[times.length - 1];
97
+ retry.count++;
98
+ const { count, max, times } = retry;
99
+ let retryTime;
100
+ if (count - 1 < times.length) {
101
+ retryTime = times[count - 1];
102
+ }
103
+ else {
104
+ const lastTime = times[times.length - 1] || 1000;
105
+ const multiplier = Math.min(Math.pow(1.5, count - times.length), 10);
106
+ const jitter = Math.floor(Math.random() * 1000);
107
+ retryTime = Math.min(Math.floor(lastTime * multiplier) + jitter, 30000);
108
+ }
93
109
  utils.logger(this.ctx).verbose(`Retry for code ${code} in ${retryTime}ms (${count}/${max})`);
94
110
  return retryTime;
95
111
  }
112
+ resetRetryCount() {
113
+ for (const retry of Object.values(this.retryCount)) {
114
+ retry.count = 0;
115
+ }
116
+ }
117
+ resetWatchdog() {
118
+ if (this.watchdogTimeout)
119
+ clearTimeout(this.watchdogTimeout);
120
+ this.watchdogTimeout = setTimeout(() => {
121
+ utils.logger(this.ctx).verbose("Watchdog timeout: no packet/pong received from Zalo WebSocket in 50s. Forcing reconnect...");
122
+ if (this.ws) {
123
+ try {
124
+ if (typeof this.ws.terminate === "function") {
125
+ this.ws.terminate();
126
+ }
127
+ else if (typeof this.ws.close === "function") {
128
+ this.ws.close(exports.CloseReason.AbnormalClosure);
129
+ }
130
+ }
131
+ catch (_a) {
132
+ // Ignore if already terminated
133
+ }
134
+ }
135
+ }, 50000);
136
+ }
96
137
  shouldRotate(code) {
97
- if (!this.ctx.settings.features.socket.rotate_error_codes.includes(code))
98
- return false;
99
- if (this.rotateCount >= this.urls.length - 1)
138
+ if (code === exports.CloseReason.AbnormalClosure) {
139
+ this.consecutive1006Count++;
140
+ if (this.consecutive1006Count >= 2) {
141
+ this.consecutive1006Count = 0;
142
+ return true;
143
+ }
100
144
  return false;
101
- return true;
145
+ }
146
+ this.consecutive1006Count = 0;
147
+ return this.ctx.settings.features.socket.rotate_error_codes.includes(code);
102
148
  }
103
149
  rotateEndpoint() {
104
- this.rotateCount++;
150
+ this.rotateCount = (this.rotateCount + 1) % this.urls.length;
105
151
  this.wsURL = utils.makeURL(this.ctx, this.urls[this.rotateCount], {
106
152
  t: Date.now(),
107
153
  });
@@ -110,37 +156,46 @@ class Listener extends EventEmitter {
110
156
  start({ retryOnClose = false } = {}) {
111
157
  if (this.ws)
112
158
  throw new ZaloApiError.ZaloApiError("Already started");
159
+ this.wsURL = utils.makeURL(this.ctx, this.urls[this.rotateCount], {
160
+ t: Date.now(),
161
+ });
162
+ const cookie = this.ctx.cookie.getCookieStringSync("https://chat.zalo.me");
113
163
  const ws = new WebSocket(this.wsURL, {
114
164
  headers: {
115
- "accept-encoding": "gzip, deflate, br, zstd",
116
- "accept-language": "en-US,en;q=0.9",
165
+ "accept-language": utils.getBrowserLanguage(this.ctx.language),
117
166
  "cache-control": "no-cache",
118
167
  connection: "Upgrade",
119
168
  host: new URL(this.wsURL).host,
120
169
  origin: "https://chat.zalo.me",
121
- prgama: "no-cache",
170
+ pragma: "no-cache",
122
171
  "sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
123
172
  "sec-websocket-version": "13",
124
173
  upgrade: "websocket",
125
174
  "user-agent": this.userAgent,
126
- cookie: this.cookie,
175
+ cookie,
127
176
  },
128
- agent: this.ctx.options.agent
177
+ agent: this.ctx.options.agent,
129
178
  });
130
179
  this.ws = ws;
131
180
  ws.onopen = () => {
181
+ this.consecutive1006Count = 0;
182
+ this.resetRetryCount();
183
+ this.resetWatchdog();
132
184
  this.onConnectedCallback();
133
185
  this.emit("connected");
134
186
  };
135
187
  ws.onclose = (event) => {
136
188
  this.reset();
137
189
  this.emit("disconnected", event.code, event.reason);
138
- const retry = retryOnClose && this.canRetry(event.code);
190
+ const retry = retryOnClose &&
191
+ this.isRetryable(event.code) &&
192
+ this.canRetry(event.code);
139
193
  if (retry && retryOnClose) {
140
194
  const shouldRotate = this.shouldRotate(event.code);
141
195
  if (shouldRotate) {
142
196
  this.rotateEndpoint();
143
197
  }
198
+ this.emit("reconnecting", event.code);
144
199
  this.retryTimeout = setTimeout(() => {
145
200
  this.start({ retryOnClose: true });
146
201
  }, retry);
@@ -155,6 +210,8 @@ class Listener extends EventEmitter {
155
210
  this.emit("error", event);
156
211
  };
157
212
  ws.onmessage = async (event) => {
213
+ var _a, _b, _c;
214
+ this.resetWatchdog();
158
215
  this.emit("pong"); // Pulse event to notify watchdog that socket is alive
159
216
  const { data } = event;
160
217
  if (!(data instanceof Buffer))
@@ -182,9 +239,10 @@ class Listener extends EventEmitter {
182
239
  this.sendWs(payload, false);
183
240
  this.emit("ping");
184
241
  };
242
+ 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);
185
243
  this.pingInterval = setInterval(() => {
186
244
  ping();
187
- }, this.ctx.settings.features.socket.ping_interval);
245
+ }, pingIntervalTime);
188
246
  }
189
247
  if (version == 1 && cmd == 501 && subCmd == 0) {
190
248
  const parsedData = (await utils.decodeEventData(parsed, this.cipherKey)).data;
@@ -381,6 +439,8 @@ class Listener extends EventEmitter {
381
439
  stop() {
382
440
  if (this.retryTimeout)
383
441
  clearTimeout(this.retryTimeout);
442
+ if (this.watchdogTimeout)
443
+ clearTimeout(this.watchdogTimeout);
384
444
  if (this.ws) {
385
445
  if (this.ws.readyState !== WebSocket.CLOSED) {
386
446
  this.ws.close(exports.CloseReason.ManualClosure);
@@ -437,6 +497,8 @@ class Listener extends EventEmitter {
437
497
  this.cipherKey = undefined;
438
498
  if (this.pingInterval)
439
499
  clearInterval(this.pingInterval);
500
+ if (this.watchdogTimeout)
501
+ clearTimeout(this.watchdogTimeout);
440
502
  }
441
503
  }
442
504
  function getHeader(buffer) {
@@ -6,7 +6,7 @@ var utils = require('../utils.cjs');
6
6
  async function login(ctx, encryptParams) {
7
7
  const encryptedParams = await getEncryptParam(ctx, encryptParams, "getlogininfo");
8
8
  try {
9
- const response = await utils.request(ctx, utils.makeURL(ctx, "https://wpa.chat.zalo.me/api/login/getLoginInfo", Object.assign(Object.assign({}, encryptedParams.params), { nretry: 0 })));
9
+ const response = await utils.request(ctx, utils.makeURL(ctx, "https://wpa.chat.zalo.me/api/login/getLoginInfo", Object.assign(Object.assign({}, encryptedParams.params), { nretry: 0 }), false));
10
10
  if (!response.ok)
11
11
  throw new ZaloApiError.ZaloApiError("Failed to fetch login info: " + response.statusText);
12
12
  const data = await response.json();
@@ -8,31 +8,16 @@ var ZaloApiLoginQRAborted = require('../Errors/ZaloApiLoginQRAborted.cjs');
8
8
  var ZaloApiLoginQRDeclined = require('../Errors/ZaloApiLoginQRDeclined.cjs');
9
9
 
10
10
  /**
11
- * Tạo bộ header Sec-CH-UA dùng chung cho toàn bộ các request trong luồng đăng nhập QR.
11
+ * 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.
12
12
  *
13
- * Trước đây 8 hàm con trong loginQR đều copy-paste cùng một bộ header với giá trị
14
- * `sec-ch-ua-platform: "Windows"` bị hardcode cứng. Điều này gây ra mâu thuẫn fingerprint
15
- * (Browser Mismatch) khi người dùng truyền vào User-Agent của Mac hoặc Linux,
16
- * khiến hệ thống chống bot của Zalo phát hiện và ban session ngay lập tức.
13
+ * Tất cả giá trị sec-ch-ua-* đều được suy luận ĐỘNG từ ctx.userAgent (chỉ khi là Chromium)
14
+ * 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.
17
15
  *
18
- * Giải pháp: Tất cả giá trị sec-ch-ua-* đều được suy luận ĐỘNG từ ctx.userAgent,
19
- * đảm bảo nhất quán 100% giữa User-Agent và các Client Hint headers.
20
- *
21
- * @param ctx - Context chứa ctx.userAgent đã được gán trước khi gọi hàm này
16
+ * @param ctx - Context chứa ctx.userAgent và ctx.language
22
17
  * @param extra - Các header bổ sung đặc thù cho từng request (sec-fetch-dest, Referer...)
23
18
  */
24
19
  function getLoginHeaders(ctx, extra) {
25
- // Suy luận platform từ UA: "Windows" | "macOS" | "Linux" (mặc định "Windows" nếu không nhận ra)
26
- const platform = utils.getPlatformFromUA(ctx.userAgent);
27
- // 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
28
- const chromeVer = utils.getChromeVersionFromUA(ctx.userAgent);
29
- 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",
30
- // sec-ch-ua phải khớp với phiên bản Chrome trong User-Agent
31
- "sec-ch-ua": `"Chromium";v="${chromeVer}", "Google Chrome";v="${chromeVer}", "Not?A_Brand";v="99"`,
32
- // Luôn là desktop (?0) vì Zalo Web không hỗ trợ mobile
33
- "sec-ch-ua-mobile": "?0",
34
- // 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
35
- "sec-ch-ua-platform": `"${platform}"` }, extra);
20
+ return Object.assign(Object.assign({ "accept-language": utils.getBrowserLanguage(ctx.language) }, utils.getSecChUaHeaders(ctx.userAgent)), extra);
36
21
  }
37
22
  exports.LoginQRCallbackEventType = void 0;
38
23
  (function (LoginQRCallbackEventType) {
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ var ZaloApiError = require('../Errors/ZaloApiError.cjs');
4
+ var utils = require('../utils.cjs');
5
+
6
+ const preloadConversationsFactory = utils.apiFactory()((api, ctx, utils) => {
7
+ const serviceURL = utils.makeURL(`${api.zpwServiceMap.conversation[0]}/api/preloadconvers/get-last-msgs`);
8
+ /**
9
+ * Preload last messages across all active conversations.
10
+ * Useful for initial state loading and catching up missed messages after reconnect.
11
+ *
12
+ * @throws {ZaloApiError}
13
+ */
14
+ return async function preloadConversations() {
15
+ const params = {
16
+ imei: ctx.imei,
17
+ };
18
+ const encryptedParams = utils.encodeAES(JSON.stringify(params));
19
+ if (!encryptedParams)
20
+ throw new ZaloApiError.ZaloApiError("Failed to encrypt params");
21
+ const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
22
+ method: "GET",
23
+ });
24
+ return utils.resolve(response);
25
+ };
26
+ });
27
+
28
+ exports.preloadConversationsFactory = preloadConversationsFactory;
package/dist/cjs/apis.cjs CHANGED
@@ -96,6 +96,7 @@ var lastOnline = require('./apis/lastOnline.cjs');
96
96
  var leaveGroup = require('./apis/leaveGroup.cjs');
97
97
  var lockPoll = require('./apis/lockPoll.cjs');
98
98
  var parseLink = require('./apis/parseLink.cjs');
99
+ var preloadConversations = require('./apis/preloadConversations.cjs');
99
100
  var rejectFriendRequest = require('./apis/rejectFriendRequest.cjs');
100
101
  var removeFriend = require('./apis/removeFriend.cjs');
101
102
  var removeFriendAlias = require('./apis/removeFriendAlias.cjs');
@@ -247,6 +248,7 @@ class API {
247
248
  this.leaveGroup = leaveGroup.leaveGroupFactory(ctx, this);
248
249
  this.lockPoll = lockPoll.lockPollFactory(ctx, this);
249
250
  this.parseLink = parseLink.parseLinkFactory(ctx, this);
251
+ this.preloadConversations = preloadConversations.preloadConversationsFactory(ctx, this);
250
252
  this.rejectFriendRequest = rejectFriendRequest.rejectFriendRequestFactory(ctx, this);
251
253
  this.removeFriend = removeFriend.removeFriendFactory(ctx, this);
252
254
  this.removeFriendAlias = removeFriendAlias.removeFriendAliasFactory(ctx, this);
@@ -12,7 +12,7 @@ class CallbacksMap extends Map {
12
12
  return super.set(key, value);
13
13
  }
14
14
  }
15
- const createContext = (apiType = 30, apiVersion = 671) => ({
15
+ const createContext = (apiType = 30, apiVersion = 689) => ({
16
16
  API_TYPE: apiType,
17
17
  API_VERSION: apiVersion,
18
18
  uploadCallbacks: new CallbacksMap(),
@@ -267,25 +267,49 @@ function getPlatformFromUA(userAgent) {
267
267
  */
268
268
  function getChromeVersionFromUA(userAgent) {
269
269
  if (!userAgent)
270
- return "130"; // Không có UA → dùng version ổn định
270
+ return "133"; // Không có UA → dùng version ổn định hiện đại
271
271
  const match = userAgent.match(/Chrome\/(\d+)/);
272
- return match ? match[1] : "130"; // Không phải Chrome → fallback về "130"
272
+ return match ? match[1] : "133"; // Không phải Chrome → fallback về "133"
273
+ }
274
+ /**
275
+ * Chuẩn hóa chuỗi Accept-Language dựa theo thiết lập ngôn ngữ ngữ cảnh.
276
+ *
277
+ * @param language Mã ngôn ngữ ("vi", "en", ...)
278
+ * @returns Chuỗi Accept-Language chuẩn theo hành vi trình duyệt
279
+ */
280
+ function getBrowserLanguage(language) {
281
+ if (language === "en") {
282
+ return "en-US,en;q=0.9,vi;q=0.8";
283
+ }
284
+ return "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7";
285
+ }
286
+ /**
287
+ * Sinh bộ header Sec-CH-UA nhất quán theo User-Agent (chỉ khi là Chromium/Chrome).
288
+ *
289
+ * Nếu User-Agent là Firefox/Safari, hàm sẽ trả về object rỗng nhằm tránh
290
+ * tạo ra dị thường Browser Mismatch.
291
+ *
292
+ * @param userAgent Chuỗi User-Agent của client
293
+ * @returns Record chứa các header sec-ch-ua-* hoặc object rỗng
294
+ */
295
+ function getSecChUaHeaders(userAgent) {
296
+ if (!userAgent || !/Chrome\//i.test(userAgent))
297
+ return {};
298
+ const platform = getPlatformFromUA(userAgent);
299
+ const chromeVer = getChromeVersionFromUA(userAgent);
300
+ return {
301
+ "sec-ch-ua": `"Not=A?Brand";v="99", "Google Chrome";v="${chromeVer}", "Chromium";v="${chromeVer}"`,
302
+ "sec-ch-ua-mobile": "?0",
303
+ "sec-ch-ua-platform": `"${platform}"`,
304
+ };
273
305
  }
274
306
  async function getDefaultHeaders(ctx, origin = "https://chat.zalo.me") {
275
307
  if (!ctx.cookie)
276
308
  throw new ZaloApiError.ZaloApiError("Cookie is not available");
277
309
  if (!ctx.userAgent)
278
310
  throw new ZaloApiError.ZaloApiError("User agent is not available");
279
- return {
280
- Accept: "application/json, text/plain, */*",
281
- "Accept-Encoding": "gzip, deflate, br, zstd",
282
- "Accept-Language": "en-US,en;q=0.9",
283
- "content-type": "application/x-www-form-urlencoded",
284
- Cookie: await ctx.cookie.getCookieString(origin),
285
- Origin: "https://chat.zalo.me",
286
- Referer: "https://chat.zalo.me/",
287
- "User-Agent": ctx.userAgent,
288
- };
311
+ const secChUa = getSecChUaHeaders(ctx.userAgent);
312
+ return Object.assign({ accept: "*/*", "accept-encoding": "gzip, deflate, br, zstd", "accept-language": getBrowserLanguage(ctx.language), "content-type": "application/x-www-form-urlencoded", cookie: await ctx.cookie.getCookieString(origin), origin: "https://chat.zalo.me", priority: "u=1, i", referer: "https://chat.zalo.me/", "user-agent": ctx.userAgent, "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site" }, secChUa);
289
313
  }
290
314
  async function request(ctx, url, options, raw = false) {
291
315
  var _a, _b;
@@ -300,11 +324,17 @@ async function request(ctx, url, options, raw = false) {
300
324
  else
301
325
  options = { headers: defaultHeaders };
302
326
  }
303
- const _options = Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), (isBun ? {
304
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
305
- // @ts-ignore
306
- proxy: (_b = (_a = ctx.options.agent) === null || _a === void 0 ? void 0 : _a.proxy) === null || _b === void 0 ? void 0 : _b.href
307
- } : { agent: ctx.options.agent }));
327
+ const _options = Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), (isBun
328
+ ? {
329
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
330
+ // @ts-ignore
331
+ proxy: (_b = (_a = ctx.options.agent) === null || _a === void 0 ? void 0 : _a.proxy) === null || _b === void 0 ? void 0 : _b.href,
332
+ }
333
+ : {
334
+ agent: ctx.options.agent,
335
+ // Hỗ trợ Undici Dispatcher cho Node.js 18+ native fetch
336
+ dispatcher: ctx.options.agent,
337
+ }));
308
338
  const response = await ctx.options.polyfill(url, _options);
309
339
  const setCookieRaw = response.headers.get("set-cookie");
310
340
  if (setCookieRaw && !raw) {
@@ -712,6 +742,7 @@ exports.encodeAES = encodeAES;
712
742
  exports.encryptPin = encryptPin;
713
743
  exports.formatTime = formatTime;
714
744
  exports.generateZaloUUID = generateZaloUUID;
745
+ exports.getBrowserLanguage = getBrowserLanguage;
715
746
  exports.getChromeVersionFromUA = getChromeVersionFromUA;
716
747
  exports.getClientMessageType = getClientMessageType;
717
748
  exports.getDefaultHeaders = getDefaultHeaders;
@@ -725,6 +756,7 @@ exports.getGroupEventType = getGroupEventType;
725
756
  exports.getImageMetaData = getImageMetaData;
726
757
  exports.getMd5LargeFileObject = getMd5LargeFileObject;
727
758
  exports.getPlatformFromUA = getPlatformFromUA;
759
+ exports.getSecChUaHeaders = getSecChUaHeaders;
728
760
  exports.getSignKey = getSignKey;
729
761
  exports.handleZaloResponse = handleZaloResponse;
730
762
  exports.hasOwn = hasOwn;
package/dist/cjs/zalo.cjs CHANGED
@@ -78,7 +78,8 @@ class Zalo {
78
78
  if (!options)
79
79
  options = {};
80
80
  if (!options.userAgent)
81
- options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0";
81
+ options.userAgent =
82
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36";
82
83
  if (!options.language)
83
84
  options.language = "vi";
84
85
  const ctx = context.createContext(this.options.apiType, this.options.apiVersion);
package/dist/context.js CHANGED
@@ -10,7 +10,7 @@ class CallbacksMap extends Map {
10
10
  return super.set(key, value);
11
11
  }
12
12
  }
13
- export const createContext = (apiType = 30, apiVersion = 671) => ({
13
+ export const createContext = (apiType = 30, apiVersion = 689) => ({
14
14
  API_TYPE: apiType,
15
15
  API_VERSION: apiVersion,
16
16
  uploadCallbacks: new CallbacksMap(),
package/dist/utils.d.ts CHANGED
@@ -81,15 +81,36 @@ export declare function getPlatformFromUA(userAgent: string | undefined): string
81
81
  * @returns Phiên bản Chrome major dạng string, ví dụ: "130", "133"
82
82
  */
83
83
  export declare function getChromeVersionFromUA(userAgent: string | undefined): string;
84
+ /**
85
+ * Chuẩn hóa chuỗi Accept-Language dựa theo thiết lập ngôn ngữ ngữ cảnh.
86
+ *
87
+ * @param language Mã ngôn ngữ ("vi", "en", ...)
88
+ * @returns Chuỗi Accept-Language chuẩn theo hành vi trình duyệt
89
+ */
90
+ export declare function getBrowserLanguage(language: string | undefined): string;
91
+ /**
92
+ * Sinh bộ header Sec-CH-UA nhất quán theo User-Agent (chỉ khi là Chromium/Chrome).
93
+ *
94
+ * Nếu User-Agent là Firefox/Safari, hàm sẽ trả về object rỗng nhằm tránh
95
+ * tạo ra dị thường Browser Mismatch.
96
+ *
97
+ * @param userAgent Chuỗi User-Agent của client
98
+ * @returns Record chứa các header sec-ch-ua-* hoặc object rỗng
99
+ */
100
+ export declare function getSecChUaHeaders(userAgent: string | undefined): Record<string, string>;
84
101
  export declare function getDefaultHeaders(ctx: ContextBase, origin?: string): Promise<{
85
- Accept: string;
86
- "Accept-Encoding": string;
87
- "Accept-Language": string;
102
+ accept: string;
103
+ "accept-encoding": string;
104
+ "accept-language": string;
88
105
  "content-type": string;
89
- Cookie: string;
90
- Origin: string;
91
- Referer: string;
92
- "User-Agent": string;
106
+ cookie: string;
107
+ origin: string;
108
+ priority: string;
109
+ referer: string;
110
+ "user-agent": string;
111
+ "sec-fetch-dest": string;
112
+ "sec-fetch-mode": string;
113
+ "sec-fetch-site": string;
93
114
  }>;
94
115
  export declare function request(ctx: ContextBase, url: string, options?: RequestInit, raw?: boolean): Promise<Response>;
95
116
  export declare function getImageMetaData(ctx: ContextBase, filePath: string): Promise<{
package/dist/utils.js CHANGED
@@ -263,25 +263,49 @@ export function getPlatformFromUA(userAgent) {
263
263
  */
264
264
  export function getChromeVersionFromUA(userAgent) {
265
265
  if (!userAgent)
266
- return "130"; // Không có UA → dùng version ổn định
266
+ return "133"; // Không có UA → dùng version ổn định hiện đại
267
267
  const match = userAgent.match(/Chrome\/(\d+)/);
268
- return match ? match[1] : "130"; // Không phải Chrome → fallback về "130"
268
+ return match ? match[1] : "133"; // Không phải Chrome → fallback về "133"
269
+ }
270
+ /**
271
+ * Chuẩn hóa chuỗi Accept-Language dựa theo thiết lập ngôn ngữ ngữ cảnh.
272
+ *
273
+ * @param language Mã ngôn ngữ ("vi", "en", ...)
274
+ * @returns Chuỗi Accept-Language chuẩn theo hành vi trình duyệt
275
+ */
276
+ export function getBrowserLanguage(language) {
277
+ if (language === "en") {
278
+ return "en-US,en;q=0.9,vi;q=0.8";
279
+ }
280
+ return "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7";
281
+ }
282
+ /**
283
+ * Sinh bộ header Sec-CH-UA nhất quán theo User-Agent (chỉ khi là Chromium/Chrome).
284
+ *
285
+ * Nếu User-Agent là Firefox/Safari, hàm sẽ trả về object rỗng nhằm tránh
286
+ * tạo ra dị thường Browser Mismatch.
287
+ *
288
+ * @param userAgent Chuỗi User-Agent của client
289
+ * @returns Record chứa các header sec-ch-ua-* hoặc object rỗng
290
+ */
291
+ export function getSecChUaHeaders(userAgent) {
292
+ if (!userAgent || !/Chrome\//i.test(userAgent))
293
+ return {};
294
+ const platform = getPlatformFromUA(userAgent);
295
+ const chromeVer = getChromeVersionFromUA(userAgent);
296
+ return {
297
+ "sec-ch-ua": `"Not=A?Brand";v="99", "Google Chrome";v="${chromeVer}", "Chromium";v="${chromeVer}"`,
298
+ "sec-ch-ua-mobile": "?0",
299
+ "sec-ch-ua-platform": `"${platform}"`,
300
+ };
269
301
  }
270
302
  export async function getDefaultHeaders(ctx, origin = "https://chat.zalo.me") {
271
303
  if (!ctx.cookie)
272
304
  throw new ZaloApiError("Cookie is not available");
273
305
  if (!ctx.userAgent)
274
306
  throw new ZaloApiError("User agent is not available");
275
- return {
276
- Accept: "application/json, text/plain, */*",
277
- "Accept-Encoding": "gzip, deflate, br, zstd",
278
- "Accept-Language": "en-US,en;q=0.9",
279
- "content-type": "application/x-www-form-urlencoded",
280
- Cookie: await ctx.cookie.getCookieString(origin),
281
- Origin: "https://chat.zalo.me",
282
- Referer: "https://chat.zalo.me/",
283
- "User-Agent": ctx.userAgent,
284
- };
307
+ const secChUa = getSecChUaHeaders(ctx.userAgent);
308
+ return Object.assign({ accept: "*/*", "accept-encoding": "gzip, deflate, br, zstd", "accept-language": getBrowserLanguage(ctx.language), "content-type": "application/x-www-form-urlencoded", cookie: await ctx.cookie.getCookieString(origin), origin: "https://chat.zalo.me", priority: "u=1, i", referer: "https://chat.zalo.me/", "user-agent": ctx.userAgent, "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site" }, secChUa);
285
309
  }
286
310
  export async function request(ctx, url, options, raw = false) {
287
311
  var _a, _b;
@@ -296,11 +320,17 @@ export async function request(ctx, url, options, raw = false) {
296
320
  else
297
321
  options = { headers: defaultHeaders };
298
322
  }
299
- const _options = Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), (isBun ? {
300
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
301
- // @ts-ignore
302
- proxy: (_b = (_a = ctx.options.agent) === null || _a === void 0 ? void 0 : _a.proxy) === null || _b === void 0 ? void 0 : _b.href
303
- } : { agent: ctx.options.agent }));
323
+ const _options = Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), (isBun
324
+ ? {
325
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
326
+ // @ts-ignore
327
+ proxy: (_b = (_a = ctx.options.agent) === null || _a === void 0 ? void 0 : _a.proxy) === null || _b === void 0 ? void 0 : _b.href,
328
+ }
329
+ : {
330
+ agent: ctx.options.agent,
331
+ // Hỗ trợ Undici Dispatcher cho Node.js 18+ native fetch
332
+ dispatcher: ctx.options.agent,
333
+ }));
304
334
  const response = await ctx.options.polyfill(url, _options);
305
335
  const setCookieRaw = response.headers.get("set-cookie");
306
336
  if (setCookieRaw && !raw) {
package/dist/zalo.js CHANGED
@@ -75,7 +75,8 @@ export class Zalo {
75
75
  if (!options)
76
76
  options = {};
77
77
  if (!options.userAgent)
78
- options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0";
78
+ options.userAgent =
79
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36";
79
80
  if (!options.language)
80
81
  options.language = "vi";
81
82
  const ctx = createContext(this.options.apiType, this.options.apiVersion);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softtynet/zca-js",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "description": "Unofficial Zalo API for JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",