@softtynet/zca-js 2.1.16 → 2.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/apis/decryptGids.d.ts +4 -0
- package/dist/apis/decryptGids.js +28 -0
- package/dist/apis/downloadTransferSync.d.ts +6 -0
- package/dist/apis/downloadTransferSync.js +30 -0
- 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/requestSyncFromPhone.d.ts +4 -0
- package/dist/apis/requestSyncFromPhone.js +27 -0
- package/dist/apis.d.ts +8 -0
- package/dist/apis.js +8 -0
- package/dist/cjs/apis/decryptGids.cjs +33 -0
- package/dist/cjs/apis/downloadTransferSync.cjs +35 -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/requestSyncFromPhone.cjs +32 -0
- package/dist/cjs/apis.cjs +8 -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/index.d.ts +3 -0
- package/dist/utils.d.ts +28 -7
- package/dist/utils.js +47 -17
- package/dist/zalo.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- [Basic Usages](#basic-usages)
|
|
19
19
|
- [Login](#login)
|
|
20
20
|
- [Listen for new messages](#listen-for-new-messages)
|
|
21
|
+
- [Synchronize Messages from Mobile](#synchronize-messages-from-mobile)
|
|
21
22
|
- [Send a message](#send-a-message)
|
|
22
23
|
- [Get/Send a sticker](#getsend-a-sticker)
|
|
23
24
|
- [Example](#example)
|
|
@@ -114,6 +115,44 @@ api.listener.start();
|
|
|
114
115
|
> [!IMPORTANT]
|
|
115
116
|
> Only one web listener can run per account at a time. If you open Zalo in the browser while the listener is active, the listener will be automatically stopped.
|
|
116
117
|
|
|
118
|
+
### Synchronize Messages from Mobile
|
|
119
|
+
|
|
120
|
+
`zca-js` supports cross-device message synchronization from the official Zalo mobile app:
|
|
121
|
+
|
|
122
|
+
#### Case 1: Auto-Sync on QR Login
|
|
123
|
+
When logging in via `loginQR`, check the **"Sync message with your phone"** checkbox on your phone before confirming:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
import { Zalo, LoginQRCallbackEventType } from "zca-js";
|
|
127
|
+
|
|
128
|
+
const zalo = new Zalo({ selfListen: true });
|
|
129
|
+
const api = await zalo.loginQR({ qrPath: "./qr.png" });
|
|
130
|
+
|
|
131
|
+
// Listen for historical messages pushed from phone
|
|
132
|
+
api.listener.on("old_messages", (messages, type) => {
|
|
133
|
+
console.log(`Received ${messages.length} historical synced messages!`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
api.listener.start();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Case 2: Manual Trigger Sync (from existing session)
|
|
140
|
+
To request message synchronization from your phone at any time:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
api.listener.on("connected", async () => {
|
|
144
|
+
// Send sync request signal to mobile phone
|
|
145
|
+
await api.requestSyncFromPhone();
|
|
146
|
+
console.log("Please tap 'Sync now' on your phone!");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
api.listener.on("old_messages", (messages, type) => {
|
|
150
|
+
console.log(`Received ${messages.length} synced messages from phone!`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
api.listener.start();
|
|
154
|
+
```
|
|
155
|
+
|
|
117
156
|
### Send a message
|
|
118
157
|
|
|
119
158
|
```javascript
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
+
import { apiFactory } from "../utils.js";
|
|
3
|
+
export const decryptGidsFactory = apiFactory()((api, ctx, utils) => {
|
|
4
|
+
const serviceURL = utils.makeURL(`${api.zpwServiceMap.profile[0]}/api/gid/decrypt`);
|
|
5
|
+
/**
|
|
6
|
+
* Giải mã danh sách Global ID (g_...) ẩn danh thành User ID thật
|
|
7
|
+
*
|
|
8
|
+
* @param gids Danh sách Global ID (hoặc 1 GID đơn lẻ)
|
|
9
|
+
* @throws {ZaloApiError}
|
|
10
|
+
*/
|
|
11
|
+
return async function decryptGids(gids) {
|
|
12
|
+
if (!gids)
|
|
13
|
+
throw new ZaloApiError("Missing gids");
|
|
14
|
+
if (!Array.isArray(gids))
|
|
15
|
+
gids = [gids];
|
|
16
|
+
const params = {
|
|
17
|
+
gids,
|
|
18
|
+
imei: ctx.imei,
|
|
19
|
+
};
|
|
20
|
+
const encryptedParams = utils.encodeAES(JSON.stringify(params));
|
|
21
|
+
if (!encryptedParams)
|
|
22
|
+
throw new ZaloApiError("Failed to encrypt params");
|
|
23
|
+
const response = await utils.request(utils.makeURL(serviceURL, { params: encryptedParams }), {
|
|
24
|
+
method: "GET",
|
|
25
|
+
});
|
|
26
|
+
return utils.resolve(response);
|
|
27
|
+
};
|
|
28
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type DownloadTransferSyncOptions = string | {
|
|
2
|
+
key: string;
|
|
3
|
+
exp: number | string;
|
|
4
|
+
sig: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const downloadTransferSyncFactory: (ctx: import("../context.js").ContextBase, api: import("../apis.js").API) => (options: DownloadTransferSyncOptions) => Promise<Buffer>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
+
import { apiFactory } from "../utils.js";
|
|
3
|
+
export const downloadTransferSyncFactory = apiFactory()((api, ctx, utils) => {
|
|
4
|
+
/**
|
|
5
|
+
* Tải khối dữ liệu snapshot đồng bộ tin nhắn từ máy chủ transfersync.zaloapp.com
|
|
6
|
+
*
|
|
7
|
+
* @param options URL đầy đủ hoặc object chứa { key, exp, sig }
|
|
8
|
+
* @throws {ZaloApiError}
|
|
9
|
+
*/
|
|
10
|
+
return async function downloadTransferSync(options) {
|
|
11
|
+
let downloadUrl = "";
|
|
12
|
+
if (typeof options === "string") {
|
|
13
|
+
downloadUrl = options;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
if (!options.key || !options.exp || !options.sig) {
|
|
17
|
+
throw new ZaloApiError("Missing key, exp or sig in options");
|
|
18
|
+
}
|
|
19
|
+
downloadUrl = `https://transfersync.zaloapp.com/api/transfer-sync-v2/download?key=${options.key}&exp=${options.exp}&sig=${options.sig}`;
|
|
20
|
+
}
|
|
21
|
+
const response = await utils.request(downloadUrl, {
|
|
22
|
+
method: "GET",
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new ZaloApiError(`Failed to download transfer snapshot: ${response.statusText} (${response.status})`);
|
|
26
|
+
}
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
return Buffer.from(arrayBuffer);
|
|
29
|
+
};
|
|
30
|
+
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { GroupMessage } from "../models/index.js";
|
|
2
2
|
export type GetGroupChatHistoryResponse = {
|
|
3
|
-
|
|
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
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
+
import { apiFactory } from "../utils.js";
|
|
3
|
+
export const requestSyncFromPhoneFactory = apiFactory()((api, ctx) => {
|
|
4
|
+
/**
|
|
5
|
+
* Gửi tín hiệu kích hoạt yêu cầu đồng bộ tin nhắn từ xa sang app điện thoại (Trường hợp 2)
|
|
6
|
+
*
|
|
7
|
+
* @throws {ZaloApiError}
|
|
8
|
+
*/
|
|
9
|
+
return async function requestSyncFromPhone() {
|
|
10
|
+
if (!api.listener) {
|
|
11
|
+
throw new ZaloApiError("WebSocket listener is not initialized. Please call api.listener.start() first.");
|
|
12
|
+
}
|
|
13
|
+
// Gửi frame yêu cầu đồng bộ qua WebSocket
|
|
14
|
+
const syncPayload = {
|
|
15
|
+
version: 1,
|
|
16
|
+
cmd: 510,
|
|
17
|
+
subCmd: 0,
|
|
18
|
+
data: {
|
|
19
|
+
act: "sync_request",
|
|
20
|
+
imei: ctx.imei,
|
|
21
|
+
ts: Date.now(),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
api.listener.sendWs(syncPayload, true);
|
|
25
|
+
return { success: true };
|
|
26
|
+
};
|
|
27
|
+
});
|
package/dist/apis.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { createNoteFactory } from "./apis/createNote.js";
|
|
|
21
21
|
import { createPollFactory } from "./apis/createPoll.js";
|
|
22
22
|
import { createProductCatalogFactory } from "./apis/createProductCatalog.js";
|
|
23
23
|
import { createReminderFactory } from "./apis/createReminder.js";
|
|
24
|
+
import { decryptGidsFactory } from "./apis/decryptGids.js";
|
|
24
25
|
import { deleteAutoReplyFactory } from "./apis/deleteAutoReply.js";
|
|
25
26
|
import { deleteAvatarFactory } from "./apis/deleteAvatar.js";
|
|
26
27
|
import { deleteCatalogFactory } from "./apis/deleteCatalog.js";
|
|
@@ -30,6 +31,7 @@ import { deleteMessageFactory } from "./apis/deleteMessage.js";
|
|
|
30
31
|
import { deleteProductCatalogFactory } from "./apis/deleteProductCatalog.js";
|
|
31
32
|
import { disableGroupLinkFactory } from "./apis/disableGroupLink.js";
|
|
32
33
|
import { disperseGroupFactory } from "./apis/disperseGroup.js";
|
|
34
|
+
import { downloadTransferSyncFactory } from "./apis/downloadTransferSync.js";
|
|
33
35
|
import { editNoteFactory } from "./apis/editNote.js";
|
|
34
36
|
import { editReminderFactory } from "./apis/editReminder.js";
|
|
35
37
|
import { enableGroupLinkFactory } from "./apis/enableGroupLink.js";
|
|
@@ -94,6 +96,7 @@ import { lastOnlineFactory } from "./apis/lastOnline.js";
|
|
|
94
96
|
import { leaveGroupFactory } from "./apis/leaveGroup.js";
|
|
95
97
|
import { lockPollFactory } from "./apis/lockPoll.js";
|
|
96
98
|
import { parseLinkFactory } from "./apis/parseLink.js";
|
|
99
|
+
import { preloadConversationsFactory } from "./apis/preloadConversations.js";
|
|
97
100
|
import { rejectFriendRequestFactory } from "./apis/rejectFriendRequest.js";
|
|
98
101
|
import { removeFriendFactory } from "./apis/removeFriend.js";
|
|
99
102
|
import { removeFriendAliasFactory } from "./apis/removeFriendAlias.js";
|
|
@@ -103,6 +106,7 @@ import { removeQuickMessageFactory } from "./apis/removeQuickMessage.js";
|
|
|
103
106
|
import { removeReminderFactory } from "./apis/removeReminder.js";
|
|
104
107
|
import { removeUnreadMarkFactory } from "./apis/removeUnreadMark.js";
|
|
105
108
|
import { removeUserFromGroupFactory } from "./apis/removeUserFromGroup.js";
|
|
109
|
+
import { requestSyncFromPhoneFactory } from "./apis/requestSyncFromPhone.js";
|
|
106
110
|
import { resetHiddenConversPinFactory } from "./apis/resetHiddenConversPin.js";
|
|
107
111
|
import { reuseAvatarFactory } from "./apis/reuseAvatar.js";
|
|
108
112
|
import { reviewPendingMemberRequestFactory } from "./apis/reviewPendingMemberRequest.js";
|
|
@@ -171,6 +175,7 @@ export declare class API {
|
|
|
171
175
|
createPoll: ReturnType<typeof createPollFactory>;
|
|
172
176
|
createProductCatalog: ReturnType<typeof createProductCatalogFactory>;
|
|
173
177
|
createReminder: ReturnType<typeof createReminderFactory>;
|
|
178
|
+
decryptGids: ReturnType<typeof decryptGidsFactory>;
|
|
174
179
|
deleteAutoReply: ReturnType<typeof deleteAutoReplyFactory>;
|
|
175
180
|
deleteAvatar: ReturnType<typeof deleteAvatarFactory>;
|
|
176
181
|
deleteCatalog: ReturnType<typeof deleteCatalogFactory>;
|
|
@@ -180,6 +185,7 @@ export declare class API {
|
|
|
180
185
|
deleteProductCatalog: ReturnType<typeof deleteProductCatalogFactory>;
|
|
181
186
|
disableGroupLink: ReturnType<typeof disableGroupLinkFactory>;
|
|
182
187
|
disperseGroup: ReturnType<typeof disperseGroupFactory>;
|
|
188
|
+
downloadTransferSync: ReturnType<typeof downloadTransferSyncFactory>;
|
|
183
189
|
editNote: ReturnType<typeof editNoteFactory>;
|
|
184
190
|
editReminder: ReturnType<typeof editReminderFactory>;
|
|
185
191
|
enableGroupLink: ReturnType<typeof enableGroupLinkFactory>;
|
|
@@ -244,6 +250,7 @@ export declare class API {
|
|
|
244
250
|
leaveGroup: ReturnType<typeof leaveGroupFactory>;
|
|
245
251
|
lockPoll: ReturnType<typeof lockPollFactory>;
|
|
246
252
|
parseLink: ReturnType<typeof parseLinkFactory>;
|
|
253
|
+
preloadConversations: ReturnType<typeof preloadConversationsFactory>;
|
|
247
254
|
rejectFriendRequest: ReturnType<typeof rejectFriendRequestFactory>;
|
|
248
255
|
removeFriend: ReturnType<typeof removeFriendFactory>;
|
|
249
256
|
removeFriendAlias: ReturnType<typeof removeFriendAliasFactory>;
|
|
@@ -253,6 +260,7 @@ export declare class API {
|
|
|
253
260
|
removeReminder: ReturnType<typeof removeReminderFactory>;
|
|
254
261
|
removeUnreadMark: ReturnType<typeof removeUnreadMarkFactory>;
|
|
255
262
|
removeUserFromGroup: ReturnType<typeof removeUserFromGroupFactory>;
|
|
263
|
+
requestSyncFromPhone: ReturnType<typeof requestSyncFromPhoneFactory>;
|
|
256
264
|
resetHiddenConversPin: ReturnType<typeof resetHiddenConversPinFactory>;
|
|
257
265
|
reuseAvatar: ReturnType<typeof reuseAvatarFactory>;
|
|
258
266
|
reviewPendingMemberRequest: ReturnType<typeof reviewPendingMemberRequestFactory>;
|