@softtynet/zca-js 2.1.11 → 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.
- package/dist/apis/getGroupChatHistory.d.ts +13 -3
- package/dist/apis/getGroupChatHistory.js +50 -22
- package/dist/apis/listen.d.ts +6 -0
- package/dist/apis/listen.js +80 -18
- package/dist/apis/login.js +1 -1
- package/dist/apis/loginQR.js +6 -21
- package/dist/apis/preloadConversations.d.ts +10 -0
- package/dist/apis/preloadConversations.js +23 -0
- package/dist/apis.d.ts +2 -0
- package/dist/apis.js +2 -0
- package/dist/cjs/apis/getGroupChatHistory.cjs +50 -22
- package/dist/cjs/apis/listen.cjs +79 -17
- package/dist/cjs/apis/login.cjs +1 -1
- package/dist/cjs/apis/loginQR.cjs +5 -20
- package/dist/cjs/apis/preloadConversations.cjs +28 -0
- package/dist/cjs/apis.cjs +2 -0
- package/dist/cjs/context.cjs +1 -1
- package/dist/cjs/utils.cjs +49 -17
- package/dist/cjs/zalo.cjs +2 -1
- package/dist/context.js +1 -1
- package/dist/utils.d.ts +28 -7
- package/dist/utils.js +47 -17
- package/dist/zalo.js +2 -1
- package/package.json +1 -1
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { GroupMessage } from "../models/index.js";
|
|
2
2
|
export type GetGroupChatHistoryResponse = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
});
|
package/dist/apis/listen.d.ts
CHANGED
|
@@ -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 }?: {
|
package/dist/apis/listen.js
CHANGED
|
@@ -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
|
-
|
|
79
|
+
var _a;
|
|
80
|
+
if (!this.isRetryable(code))
|
|
75
81
|
return false;
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
const { count, max, times } =
|
|
80
|
-
|
|
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 (
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
},
|
|
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) {
|
package/dist/apis/login.js
CHANGED
|
@@ -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();
|
package/dist/apis/loginQR.js
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
package/dist/cjs/apis/listen.cjs
CHANGED
|
@@ -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
|
-
|
|
91
|
+
var _a;
|
|
92
|
+
if (!this.isRetryable(code))
|
|
87
93
|
return false;
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
const { count, max, times } =
|
|
92
|
-
|
|
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 (
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
},
|
|
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) {
|
package/dist/cjs/apis/login.cjs
CHANGED
|
@@ -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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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);
|
package/dist/cjs/context.cjs
CHANGED
|
@@ -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 =
|
|
15
|
+
const createContext = (apiType = 30, apiVersion = 689) => ({
|
|
16
16
|
API_TYPE: apiType,
|
|
17
17
|
API_VERSION: apiVersion,
|
|
18
18
|
uploadCallbacks: new CallbacksMap(),
|
package/dist/cjs/utils.cjs
CHANGED
|
@@ -267,25 +267,49 @@ function getPlatformFromUA(userAgent) {
|
|
|
267
267
|
*/
|
|
268
268
|
function getChromeVersionFromUA(userAgent) {
|
|
269
269
|
if (!userAgent)
|
|
270
|
-
return "
|
|
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] : "
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
86
|
-
"
|
|
87
|
-
"
|
|
102
|
+
accept: string;
|
|
103
|
+
"accept-encoding": string;
|
|
104
|
+
"accept-language": string;
|
|
88
105
|
"content-type": string;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 "
|
|
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] : "
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 =
|
|
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);
|