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