@softtynet/zca-js 2.2.6 → 2.2.8
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 +159 -177
- package/dist/cjs/update.cjs +8 -6
- package/dist/update.js +8 -6
- package/docs/INTEGRATION_GUIDE_SYNC_AND_CALL.md +371 -0
- package/package.json +12 -9
- package/CODE_OF_CONDUCT.md +0 -163
- package/CONTRIBUTING.md +0 -470
- package/SECURITY.md +0 -83
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Hướng dẫn Tích hợp: Đồng bộ Tin nhắn Đa Thiết bị (`ZDB4.0`) & Gọi Thoại / Video (`@softtynet/zca-js`)
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> Tài liệu này hướng dẫn chi tiết cách tích hợp các tính năng nâng cấp trên nhánh `lib` của `@softtynet/zca-js` (từ phiên bản `v2.2.0+`), đã được **kiểm định thực tế 100% trên máy chủ Zalo thật và thiết bị di động thật**.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Mục lục
|
|
9
|
+
|
|
10
|
+
1. [Tổng quan Kiến trúc](#1-tổng-quan-kiến-trúc)
|
|
11
|
+
2. [Bảng tra cứu nhanh API, Sự kiện & Hàm Giải mã](#2-bảng-tra-cứu-nhanh-api-sự-kiện--hàm-giải-mã)
|
|
12
|
+
3. [Tích hợp Tầng 1: Đồng bộ bù tin nhắn khi Offline (Catch-up Sync)](#3-tích-hợp-tầng-1-đồng-bộ-bù-tin-nhắn-khi-offline-catch-up-sync)
|
|
13
|
+
4. [Tích hợp Tầng 2: Đồng bộ lịch sử sao lưu từ Điện thoại (`ZDB4.0`)](#4-tích-hợp-tầng-2-đồng-bộ-lịch-sử-sao-lưu-từ-điện-thoại-zdb40)
|
|
14
|
+
5. [Ánh xạ ID ẩn danh (`getNoisedIds` & `decryptGids`)](#5-ánh-xạ-id-ẩn-danh-getnoisedids--decryptgids)
|
|
15
|
+
6. [Tích hợp Gọi Thoại (`CallType.Audio`) & Gọi Video (`CallType.Video`)](#6-tích-hợp-gọi-thoại-calltypeaudio--gọi-video-calltypevideo)
|
|
16
|
+
7. [Các Lưu ý Quan trọng khi Vận hành Thực tế](#7-các-lưu-ý-quan-trọng-khi-vận-hành-thực-tế)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Tổng quan Kiến trúc
|
|
21
|
+
|
|
22
|
+
Hệ thống đồng bộ và cuộc gọi của Zalo hoạt động theo 3 luồng độc lập:
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart TD
|
|
26
|
+
subgraph Tầng 1: Catch-up Sync (Khi mở lại App / Mất kết nối ngắn)
|
|
27
|
+
A1["api.preloadConversations(threadIdLocalMsgId)"] --> A2["Trả về messages + unreadCleared (cmd 10053)"]
|
|
28
|
+
A3["WebSocket: sự kiện cipher_key"] --> A4["api.listener.requestOldMessages(User / Group)"]
|
|
29
|
+
A4 --> A5["Sự kiện old_messages (cmd 510 / 511) + hasMore"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph Tầng 2: Mobile Backup Sync (Kéo lịch sử từ Điện thoại)
|
|
33
|
+
B1["api.requestSyncFromPhone() (cmd 12000)"] --> B2["Điện thoại hiện thông báo xác nhận"]
|
|
34
|
+
B2 --> B3["WebSocket: sync_event (user_confirm -> syncmsg_info)"]
|
|
35
|
+
B3 --> B4["api.getSyncData() tải file .db.crypt"]
|
|
36
|
+
B4 --> B5["decryptWithPrivateKey -> deriveZdbAesKey -> decryptZdbBuffer -> parseZdbHeader"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
subgraph Luồng 3: Gọi Thoại & Video (Signaling 2 bước)
|
|
40
|
+
C1["api.sendVoiceCall(calleeId, { callType })"] --> C2["Bước 1: /api/voicecall/requestcall (cmd 11300)"]
|
|
41
|
+
C2 --> C3["Bước 2: /api/voicecall/request (cmd 11301)"]
|
|
42
|
+
C3 --> C4["Đổ chuông Thoại (1) hoặc Video (3) trên máy người nhận"]
|
|
43
|
+
C4 --> C5["api.cancelVoiceCall (11305) / api.endVoiceCall (11306)"]
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Bảng tra cứu nhanh API, Sự kiện & Hàm Giải mã
|
|
50
|
+
|
|
51
|
+
### 2.1. Các phương thức trên `api` (`API` instance)
|
|
52
|
+
|
|
53
|
+
| Phương thức | Command ID | Endpoint Zalo | Công dụng |
|
|
54
|
+
| :--- | :---: | :--- | :--- |
|
|
55
|
+
| [`api.preloadConversations(threadIdLocalMsgId?)`](../src/apis/preloadConversations.ts) | `10053` | `/api/preloadconvers/get-last-msgs` | Lấy danh sách tin nhắn mới nhất của các hội thoại (`messages`, `msgs`, `groupMsgs`) và danh sách đã đọc (`unreadCleared`). |
|
|
56
|
+
| [`api.requestSyncFromPhone(options?)`](../src/apis/requestSyncFromPhone.ts) | `12000` | `/api/message/pull_mobile_msg` | Tự sinh cặp khoá RSA-2048 và gửi yêu cầu đồng bộ lịch sử tin nhắn về điện thoại. Trả về `{ privateKey, publicKeyBase64, pcName }`. |
|
|
57
|
+
| [`api.pullMobile(options)`](../src/apis/pullMobile.ts) | `12000` | `/api/message/pull_mobile_msg` | API mức thấp để gửi yêu cầu đồng bộ với `public_key` tự quản lý. |
|
|
58
|
+
| [`api.cancelPullMobile(options)`](../src/apis/cancelPullMobile.ts) | `12700` | `/api/message/cancel_pull_mobile_msg` | Thu hồi thông báo yêu cầu đồng bộ trên điện thoại (khi hết thời gian chờ hoặc người dùng bấm huỷ trên Web/PC). |
|
|
59
|
+
| [`api.getSyncData(options)`](../src/apis/getSyncData.ts) | — | *(URL động từ `syncmsg_info`)* | Tải gói sao lưu `.db.crypt` do điện thoại tải lên về thư mục máy chủ (`savePath`). |
|
|
60
|
+
| [`api.getNoisedIds(options)`](../src/apis/getNoisedIds.ts) | `12390` | `/api/znoise` *(có fallback `zwid.api.zalo.me`)* | Chuyển đổi danh sách User ID (`fids`) và Group ID (`gids`) thật sang mã ẩn danh (Noised ID) dùng trong tên file SQLite của `ZDB4.0`. |
|
|
61
|
+
| [`api.decryptGids(gids)`](../src/apis/decryptGids.ts) | `12054` | `/api/gid/decrypt` | Giải mã danh sách Global ID / Noised ID về ID gốc. |
|
|
62
|
+
| [`api.sendVoiceCall(calleeId, options?)`](../src/apis/sendVoiceCall.ts) | `11300` + `11301` | `/api/voicecall/requestcall` + `/api/voicecall/request` | Khởi tạo cuộc gọi Thoại (`CallType.Audio = 1`) hoặc Video (`CallType.Video = 3`), làm đổ chuông thiết bị người nhận. |
|
|
63
|
+
| [`api.cancelVoiceCall(calleeId, callId, options?)`](../src/apis/cancelVoiceCall.ts) | `11305` | `/api/voicecall/cancel` | Huỷ cuộc gọi đang đổ chuông trước khi người nhận bắt máy. |
|
|
64
|
+
| [`api.endVoiceCall(calleeId, callId, options?)`](../src/apis/endVoiceCall.ts) | `11306` | `/api/voicecall/endcall` | Kết thúc cuộc gọi đang diễn ra. |
|
|
65
|
+
|
|
66
|
+
### 2.2. Các sự kiện WebSocket trên `api.listener`
|
|
67
|
+
|
|
68
|
+
| Tên sự kiện | WebSocket `cmd` | Tham số Callback | Mô tả |
|
|
69
|
+
| :--- | :---: | :--- | :--- |
|
|
70
|
+
| `old_messages` | `510` (User) / `511` (Group) | `(messages: Message[], type: ThreadType, hasMore?: boolean)` | Phát ra khi nhận lô tin nhắn cũ từ hàng đợi offline của Zalo. Khi `hasMore === true`, còn tin nhắn tiếp theo cần kéo tiếp. |
|
|
71
|
+
| `unread_cleared` | `504` / `524` | `(items: ClearUnread[])` | Phát ra khi người dùng đọc tin nhắn trên thiết bị khác (đồng bộ trạng thái đã đọc). |
|
|
72
|
+
| `sync_event` | `601` (`syncmsgmb` / `transfer_msg`) | `(event: SyncEvent)` | Phát ra các trạng thái đồng bộ từ điện thoại: `user_confirm`, `syncmsg_info`, `transfer_after_login`, `transfer_error`. |
|
|
73
|
+
| `call_event` | `601` (`voip`) | `(event: CallEvent)` | Phát ra tín hiệu cuộc gọi thời gian thực: `request`, `ringring`, `answer`, `cancel`, `endcall`, ... |
|
|
74
|
+
|
|
75
|
+
### 2.3. Các hàm tiện ích mật mã (`ZDB4.0` Crypto Helpers)
|
|
76
|
+
|
|
77
|
+
Được export trực tiếp từ `@softtynet/zca-js`:
|
|
78
|
+
- [`generateRSAKeyPair()`](../src/utils.ts): Sinh cặp khoá RSA-2048 (`publicKey`, `privateKey`, `publicKeyBase64`).
|
|
79
|
+
- [`decryptWithPrivateKey(privateKeyPem, encryptedBase64)`](../src/utils.ts): Giải mã `encrypted_key` bằng RSA PKCS#1 v1.5 (tự động dự phòng bằng thuật toán BigInt CRT thuần JS trên Node.js 20/22/24+).
|
|
80
|
+
- [`deriveZdbAesKey(decryptedRsaBytes)`](../src/utils.ts): Rút trích khoá AES-256 (32 bytes) từ payload RSA đã giải mã.
|
|
81
|
+
- [`decryptZdbBuffer(encryptedBuf, aesKey)`](../src/utils.ts): Giải mã toàn bộ nội dung file `.db.crypt` theo từng khối `64KB` (`0x10000` bytes) với `AES-256-CBC` và `IV = 16 bytes 0x00`.
|
|
82
|
+
- [`parseZdbHeader(zdbPlain)`](../src/utils.ts): Đọc header `"ZDB4.0"`, trích xuất danh sách toàn bộ file SQLite (`entries: [{ name, size }]`) và vị trí bắt đầu luồng nén XZ (`xzOffset`).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Tích hợp Tầng 1: Đồng bộ bù tin nhắn khi Offline (Catch-up Sync)
|
|
87
|
+
|
|
88
|
+
Khi ứng dụng của bạn vừa khởi động lại hoặc vừa khôi phục kết nối mạng, hãy kết hợp **2 bước**:
|
|
89
|
+
1. Gọi `api.preloadConversations(threadIdLocalMsgId)` qua HTTP để lấy nhanh tin nhắn cuối cùng của các hội thoại và danh sách đã đọc (`unreadCleared`).
|
|
90
|
+
2. Sau khi WebSocket phát sự kiện **`cipher_key`**, gọi `api.listener.requestOldMessages(...)` để kéo toàn bộ hàng đợi tin nhắn cá nhân (`cmd 510`) và nhóm (`cmd 511`). Nếu callback `old_messages` trả về `hasMore === true`, tiếp tục truyền `msgId` cuối cùng vào `requestOldMessages(type, lastMsgId)` để phân trang đến hết.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { Zalo, ThreadType } from "@softtynet/zca-js";
|
|
94
|
+
|
|
95
|
+
const zalo = new Zalo({ selfListen: true });
|
|
96
|
+
const api = await zalo.login(credentials);
|
|
97
|
+
|
|
98
|
+
// Bước 1: Preload tin nhắn mới nhất & trạng thái đã đọc qua HTTP (command 10053)
|
|
99
|
+
// Có thể truyền map { [threadId]: lastLocalMsgId } đã lưu trong DB của bạn, hoặc bỏ trống {}
|
|
100
|
+
const preload = await api.preloadConversations({});
|
|
101
|
+
|
|
102
|
+
for (const msg of preload.messages) {
|
|
103
|
+
console.log(`[Preload] thread=${msg.threadId} msgId=${msg.data.msgId}:`, msg.data.content);
|
|
104
|
+
}
|
|
105
|
+
for (const cleared of preload.unreadCleared) {
|
|
106
|
+
console.log(`[Preload UnreadCleared] thread=${cleared.idTo} lastMsgId=${cleared.lastMsgId} isGroup=${cleared.isGroup}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Bước 2: Lắng nghe hàng đợi tin nhắn cũ qua WebSocket (cmd 510 & 511)
|
|
110
|
+
api.listener.on("old_messages", (messages, type, hasMore) => {
|
|
111
|
+
console.log(`[OldMessages] Nhận ${messages.length} tin nhắn (${type === ThreadType.Group ? "Nhóm" : "Cá nhân"}), hasMore=${hasMore}`);
|
|
112
|
+
|
|
113
|
+
for (const msg of messages) {
|
|
114
|
+
// Lưu tin nhắn vào Database của bạn (upsert theo msg.data.msgId)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Nếu còn tin nhắn trong hàng đợi (hasMore === true), tự động kéo trang tiếp theo
|
|
118
|
+
if (hasMore && messages.length > 0) {
|
|
119
|
+
const lastMsgId = messages[messages.length - 1].data.msgId;
|
|
120
|
+
api.listener.requestOldMessages(type, lastMsgId);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Đồng bộ trạng thái đã đọc thời gian thực khi bạn đọc tin nhắn trên điện thoại
|
|
125
|
+
api.listener.on("unread_cleared", (items) => {
|
|
126
|
+
for (const item of items) {
|
|
127
|
+
console.log(`[UnreadCleared] Đã đọc hội thoại ${item.idTo} tới msgId=${item.lastMsgId}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// QUAN TRỌNG: Chỉ gọi requestOldMessages sau khi nhận sự kiện "cipher_key"
|
|
132
|
+
api.listener.on("cipher_key", () => {
|
|
133
|
+
api.listener.requestOldMessages(ThreadType.User); // mặc định gửi lastId = "10000000000000000"
|
|
134
|
+
api.listener.requestOldMessages(ThreadType.Group); // mặc định gửi lastId = "10000000000000000"
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
api.listener.start({ retryOnClose: true });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 4. Tích hợp Tầng 2: Đồng bộ lịch sử sao lưu từ Điện thoại (`ZDB4.0`)
|
|
143
|
+
|
|
144
|
+
Tính năng này cho phép kéo toàn bộ lịch sử tin nhắn đang nằm trong máy điện thoại của người dùng lên Web/PC (giống hệt nút **"Đồng bộ tin nhắn"** trên Zalo PC/Web).
|
|
145
|
+
|
|
146
|
+
Có **2 kịch bản kích hoạt** đều được hỗ trợ đầy đủ:
|
|
147
|
+
- **Kịch bản A (Tự động sau khi quét QR)**: Nếu lúc quét mã QR trên điện thoại, người dùng tích chọn *"Đồng bộ tin nhắn"*, Zalo sẽ bắn sự kiện `SyncEventType.TRANSFER_AFTER_LOGIN` kèm `temp_key`. Bạn chỉ cần gọi `api.requestSyncFromPhone({ tempKey: event.data.temp_key })` — điện thoại sẽ tự đẩy dữ liệu lên mà không cần hỏi lại!
|
|
148
|
+
- **Kịch bản B (Chủ động yêu cầu bất kỳ lúc nào)**: Gọi `api.requestSyncFromPhone({ pcName: "Web" })` để làm nổ thông báo *"Xác nhận đồng bộ tin nhắn"* trên điện thoại người dùng. Nếu sau 60 giây người dùng không bấm, gọi `api.cancelPullMobile(...)` để đóng thông báo trên điện thoại.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import fs from "node:fs";
|
|
152
|
+
import {
|
|
153
|
+
SyncEventType,
|
|
154
|
+
decryptWithPrivateKey,
|
|
155
|
+
deriveZdbAesKey,
|
|
156
|
+
decryptZdbBuffer,
|
|
157
|
+
parseZdbHeader,
|
|
158
|
+
} from "@softtynet/zca-js";
|
|
159
|
+
|
|
160
|
+
let activePrivateKey = "";
|
|
161
|
+
let activePublicKeyBase64 = "";
|
|
162
|
+
let activePcName = "Web";
|
|
163
|
+
let confirmTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
164
|
+
|
|
165
|
+
// 1. Lắng nghe toàn bộ tiến trình đồng bộ từ điện thoại qua sự kiện "sync_event"
|
|
166
|
+
api.listener.on("sync_event", async (event) => {
|
|
167
|
+
switch (event.type) {
|
|
168
|
+
// Kịch bản A: Người dùng tích chọn "Đồng bộ tin nhắn" ngay lúc quét QR
|
|
169
|
+
case SyncEventType.TRANSFER_AFTER_LOGIN: {
|
|
170
|
+
const session = await api.requestSyncFromPhone({
|
|
171
|
+
tempKey: event.data.temp_key,
|
|
172
|
+
pcName: "Web",
|
|
173
|
+
});
|
|
174
|
+
activePrivateKey = session.privateKey;
|
|
175
|
+
activePublicKeyBase64 = session.publicKeyBase64;
|
|
176
|
+
activePcName = session.pcName;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Người dùng vừa bấm "Đồng bộ ngay" trên điện thoại (user_action === 1)
|
|
181
|
+
case SyncEventType.USER_CONFIRM: {
|
|
182
|
+
if (confirmTimeout) clearTimeout(confirmTimeout);
|
|
183
|
+
if (event.data.user_action === 1) {
|
|
184
|
+
console.log("📱 Người dùng đã xác nhận! Đang chờ điện thoại đóng gói và tải file lên...");
|
|
185
|
+
} else {
|
|
186
|
+
console.log("⚠️ Người dùng đã từ chối đồng bộ trên điện thoại.");
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Điện thoại đã tải xong file .db.crypt lên server Zalo
|
|
192
|
+
case SyncEventType.SYNCMSG_INFO: {
|
|
193
|
+
if (confirmTimeout) clearTimeout(confirmTimeout);
|
|
194
|
+
console.log(`📦 Gói sao lưu sẵn sàng: ${event.data.file_name} (${event.data.file_size} bytes)`);
|
|
195
|
+
|
|
196
|
+
// Tải file .db.crypt về thư mục ./sync_data
|
|
197
|
+
const dl = await api.getSyncData({
|
|
198
|
+
...event.data,
|
|
199
|
+
savePath: "./sync_data",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (dl.success && dl.data && activePrivateKey) {
|
|
203
|
+
// Giải mã khoá RSA PKCS#1 v1.5 -> Rút trích khoá AES-256
|
|
204
|
+
const rawRsa = decryptWithPrivateKey(activePrivateKey, dl.data.encrypted_key);
|
|
205
|
+
const aesKey = deriveZdbAesKey(rawRsa);
|
|
206
|
+
|
|
207
|
+
// Giải mã toàn bộ container ZDB4.0 (chia từng khối 64KB với IV=0)
|
|
208
|
+
const cryptBytes = fs.readFileSync(dl.data.file_path);
|
|
209
|
+
const zdbPlain = decryptZdbBuffer(cryptBytes, aesKey);
|
|
210
|
+
|
|
211
|
+
// Đọc mục lục (TOC) các file SQLite hội thoại bên trong gói ZDB4.0
|
|
212
|
+
const toc = parseZdbHeader(zdbPlain);
|
|
213
|
+
console.log(`🎉 Giải mã thành công ${toc.entries.length} file SQLite hội thoại!`, toc.entries);
|
|
214
|
+
|
|
215
|
+
// Lưu ý: Luồng nén XZ chứa dữ liệu của các file SQLite bắt đầu tại:
|
|
216
|
+
const xzStream = zdbPlain.subarray(toc.xzOffset);
|
|
217
|
+
fs.writeFileSync(`${dl.data.file_path}.xz`, xzStream);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case SyncEventType.TRANSFER_ERROR: {
|
|
223
|
+
console.error("❌ Lỗi đồng bộ từ điện thoại:", event.data.error_code, event.data.message);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 2. Hàm chủ động bấm nút "Đồng bộ từ điện thoại" (Kịch bản B)
|
|
230
|
+
async function triggerManualPhoneSync() {
|
|
231
|
+
const session = await api.requestSyncFromPhone({ pcName: "Web" });
|
|
232
|
+
activePrivateKey = session.privateKey;
|
|
233
|
+
activePublicKeyBase64 = session.publicKeyBase64;
|
|
234
|
+
activePcName = session.pcName;
|
|
235
|
+
|
|
236
|
+
// Tự động thu hồi thông báo trên điện thoại nếu sau 60s người dùng không bấm
|
|
237
|
+
confirmTimeout = setTimeout(async () => {
|
|
238
|
+
await api.cancelPullMobile({
|
|
239
|
+
public_key: activePublicKeyBase64,
|
|
240
|
+
pc_name: activePcName,
|
|
241
|
+
});
|
|
242
|
+
console.log("🧹 Đã thu hồi yêu cầu đồng bộ trên điện thoại do quá thời gian chờ.");
|
|
243
|
+
}, 60_000);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 5. Ánh xạ ID ẩn danh (`getNoisedIds` & `decryptGids`)
|
|
250
|
+
|
|
251
|
+
Bên trong gói sao lưu `ZDB4.0`, Zalo đặt tên các file SQLite theo mẫu:
|
|
252
|
+
- Tin nhắn cá nhân: `3_<myNoisedId>_<friendNoisedId>.db`
|
|
253
|
+
- Tin nhắn nhóm: `4_<myNoisedId>_<groupNoisedId>.db`
|
|
254
|
+
|
|
255
|
+
Để biết file `.db` nào ứng với bạn bè (`uid`) hay nhóm (`groupId`) nào, sử dụng [`api.getNoisedIds`](../src/apis/getNoisedIds.ts) và [`api.decryptGids`](../src/apis/decryptGids.ts):
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// 1. Đổi danh sách UID bạn bè & Group ID thật sang Noised ID
|
|
259
|
+
const ownId = api.getOwnId();
|
|
260
|
+
const friendIds = ["5249661060655759359", "6912185345590173640"];
|
|
261
|
+
const groupIds = ["7890123456789012345"];
|
|
262
|
+
|
|
263
|
+
const noised = await api.getNoisedIds({
|
|
264
|
+
fids: [ownId, ...friendIds],
|
|
265
|
+
gids: groupIds,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const myNoisedId = noised.fids[0];
|
|
269
|
+
console.log("My Noised ID:", myNoisedId);
|
|
270
|
+
|
|
271
|
+
// Map ngược từ tên file SQLite sang Friend UID thật:
|
|
272
|
+
const sqliteFileToFriendUid = new Map<string, string>();
|
|
273
|
+
friendIds.forEach((uid, index) => {
|
|
274
|
+
const friendNoised = noised.fids[index + 1];
|
|
275
|
+
if (friendNoised) {
|
|
276
|
+
sqliteFileToFriendUid.set(`3_${myNoisedId}_${friendNoised}.db`, uid);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 2. Giải mã trực tiếp danh sách Global ID / Noised ID qua /api/gid/decrypt
|
|
281
|
+
const decrypted = await api.decryptGids(noised.fids);
|
|
282
|
+
console.log("Decrypted mapping:", decrypted.data ?? decrypted.gids);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 6. Tích hợp Gọi Thoại (`CallType.Audio`) & Gọi Video (`CallType.Video`)
|
|
288
|
+
|
|
289
|
+
Thư viện hỗ trợ khởi tạo cuộc gọi 2 bước chuẩn giao thức C++ `ZaloCall` của Zalo PC:
|
|
290
|
+
- **`CallType.Audio = 1`**: Cuộc gọi thoại 1-1.
|
|
291
|
+
- **`CallType.Video = 3`**: Cuộc gọi video 1-1 (tự động đính kèm cấu hình codec H.264 `payload: 97` trong `extendData`).
|
|
292
|
+
|
|
293
|
+
### 6.1. Gọi đi (Đổ chuông Thoại hoặc Video) & Tự động huỷ khi không bắt máy
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { CallType } from "@softtynet/zca-js";
|
|
297
|
+
|
|
298
|
+
const targetUid = "5249661060655759359"; // Luôn truyền dạng chuỗi (string) 19 chữ số
|
|
299
|
+
|
|
300
|
+
// --- A. Khởi tạo Cuộc gọi VIDEO ---
|
|
301
|
+
const videoCall = await api.sendVoiceCall(targetUid, {
|
|
302
|
+
callType: CallType.Video, // = 3
|
|
303
|
+
});
|
|
304
|
+
console.log("Đang đổ chuông Video:", {
|
|
305
|
+
callId: videoCall.callId,
|
|
306
|
+
sessId: videoCall.sessId,
|
|
307
|
+
rtpServer: videoCall.rtpServer,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Huỷ cuộc gọi Video sau 30 giây nếu người nhận chưa bắt máy
|
|
311
|
+
const ringTimer = setTimeout(async () => {
|
|
312
|
+
await api.cancelVoiceCall(targetUid, videoCall.callId, {
|
|
313
|
+
callType: CallType.Video,
|
|
314
|
+
});
|
|
315
|
+
console.log("Đã huỷ cuộc gọi Video do hết thời gian chờ.");
|
|
316
|
+
}, 30_000);
|
|
317
|
+
|
|
318
|
+
// --- B. Khởi tạo Cuộc gọi THOẠI ---
|
|
319
|
+
const audioCall = await api.sendVoiceCall(targetUid, {
|
|
320
|
+
callType: CallType.Audio, // = 1 (mặc định)
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Kết thúc cuộc gọi đang diễn ra bất kỳ lúc nào
|
|
324
|
+
await api.endVoiceCall(targetUid, audioCall.callId);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 6.2. Lắng nghe sự kiện Cuộc gọi thời gian thực (`call_event`)
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { CallEventType } from "@softtynet/zca-js";
|
|
331
|
+
|
|
332
|
+
api.listener.on("call_event", (event) => {
|
|
333
|
+
console.log(`[VoIP Signal] type=${event.type}`, event.data);
|
|
334
|
+
|
|
335
|
+
switch (event.type) {
|
|
336
|
+
case CallEventType.RINGRING:
|
|
337
|
+
console.log(`🔔 Máy của người nhận đã bắt đầu đổ chuông (callId=${event.data.callId})`);
|
|
338
|
+
break;
|
|
339
|
+
case CallEventType.ANSWER:
|
|
340
|
+
console.log(`🟢 Người nhận đã bắt máy (callId=${event.data.callId})`);
|
|
341
|
+
break;
|
|
342
|
+
case CallEventType.CANCEL:
|
|
343
|
+
case CallEventType.ENDCALL:
|
|
344
|
+
console.log(`🔴 Cuộc gọi đã kết thúc hoặc bị từ chối (callId=${event.data.callId})`);
|
|
345
|
+
break;
|
|
346
|
+
case CallEventType.INCOMING_REQUEST:
|
|
347
|
+
console.log(`📲 Có cuộc gọi đến từ UID ${event.data.callerId} (callId=${event.data.callId})`);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 7. Các Lưu ý Quan trọng khi Vận hành Thực tế
|
|
356
|
+
|
|
357
|
+
> [!WARNING]
|
|
358
|
+
> **1. Tuyệt đối giữ UID ở kiểu `string` (không ép kiểu `Number`)**
|
|
359
|
+
> UID của Zalo có độ dài 19 chữ số (ví dụ `"5249661060655759359"`), vượt quá giới hạn `Number.MAX_SAFE_INTEGER` (`9007199254740991`) của JavaScript. Nếu bạn ép kiểu `Number(uid)` trước khi truyền vào hàm, 3 chữ số cuối sẽ bị làm tròn thành `000` và máy chủ Zalo sẽ trả lỗi `ZaloApiError: Tham số không hợp lệ (code: 114)`.
|
|
360
|
+
|
|
361
|
+
> [!TIP]
|
|
362
|
+
> **2. Thời điểm gọi `requestOldMessages`**
|
|
363
|
+
> Luôn gọi `api.listener.requestOldMessages(...)` bên trong sự kiện `api.listener.on("cipher_key", ...)` thay vì `on("connected")`. Khi sự kiện `connected` vừa nổ, kết nối TCP/WebSocket mới thiết lập xong nhưng máy chủ Zalo chưa gửi gói `cmd=1, subCmd=1` chứa `cipher_key`.
|
|
364
|
+
|
|
365
|
+
> [!NOTE]
|
|
366
|
+
> **3. Kịch bản kiểm định nhanh trên máy của bạn**
|
|
367
|
+
> Bất kỳ lúc nào muốn kiểm tra lại toàn bộ tính năng trên tài khoản thật, bạn có thể chạy:
|
|
368
|
+
> - Kiểm định tự động (Offline/Unit): `npm run test:verify`
|
|
369
|
+
> - Kiểm định toàn diện trên Server thật (6 bước): `npm run test:live`
|
|
370
|
+
> - Chỉ test nhanh Gọi Thoại + Video tới 1 UID: `npm run test:live -- --only-call --call <UID>`
|
|
371
|
+
> - Chỉ test riêng Gọi Video tới 1 UID: `npm run test:live -- --only-call --video-only --call <UID>`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softtynet/zca-js",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.8",
|
|
4
|
+
"description": "Bộ thư viện Zalo API nâng cao phát triển bởi Softtynet (Đồng bộ tin nhắn đa thiết bị ZDB4.0, Catch-up Sync, Voice & Video Call Signaling)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"build": "npm run build:clean && npm run build:esm && npm run build:cjs",
|
|
20
20
|
"test:verify": "npx tsx test/verify_upgrades.ts",
|
|
21
21
|
"test:live": "npx tsx test/verify_live.ts",
|
|
22
|
-
"test:feat": "npm run test/feat.ts",
|
|
23
22
|
"prettier": "prettier --write .",
|
|
24
23
|
"lint": "eslint .",
|
|
25
24
|
"lint:fix": "eslint . --fix",
|
|
@@ -27,21 +26,25 @@
|
|
|
27
26
|
},
|
|
28
27
|
"repository": {
|
|
29
28
|
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/
|
|
29
|
+
"url": "git+https://github.com/softtyos/zca-js.git"
|
|
31
30
|
},
|
|
32
|
-
"homepage": "https://github.com/
|
|
31
|
+
"homepage": "https://github.com/softtyos/zca-js",
|
|
33
32
|
"bugs": {
|
|
34
|
-
"url": "https://github.com/
|
|
33
|
+
"url": "https://github.com/softtyos/zca-js/issues"
|
|
35
34
|
},
|
|
36
35
|
"engines": {
|
|
37
36
|
"node": ">=18.0.0"
|
|
38
37
|
},
|
|
39
38
|
"keywords": [
|
|
40
|
-
"chatbot",
|
|
41
39
|
"zalo",
|
|
42
|
-
"api"
|
|
40
|
+
"zalo-api",
|
|
41
|
+
"zca-js",
|
|
42
|
+
"softtynet",
|
|
43
|
+
"zalo-sync",
|
|
44
|
+
"zalo-call",
|
|
45
|
+
"chatbot"
|
|
43
46
|
],
|
|
44
|
-
"author": "RFS-ADRENO
|
|
47
|
+
"author": "Softtynet Co., Ltd. (Based on zca-js by RFS-ADRENO)",
|
|
45
48
|
"license": "MIT",
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"crypto-js": "^4.2.0",
|
package/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# Code of Conduct
|
|
2
|
-
|
|
3
|
-
## Our Pledge
|
|
4
|
-
|
|
5
|
-
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
-
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
-
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
-
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
-
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
-
and orientation.
|
|
11
|
-
|
|
12
|
-
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
-
diverse, inclusive, and healthy community.
|
|
14
|
-
|
|
15
|
-
## Our Standards
|
|
16
|
-
|
|
17
|
-
Examples of behavior that contributes to a positive environment for our
|
|
18
|
-
community include:
|
|
19
|
-
|
|
20
|
-
* Demonstrating empathy and kindness toward other people
|
|
21
|
-
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
-
* Giving and gracefully accepting constructive feedback
|
|
23
|
-
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
-
and learning from the experience
|
|
25
|
-
* Focusing on what is best not just for us as individuals, but for the
|
|
26
|
-
overall community
|
|
27
|
-
|
|
28
|
-
Examples of unacceptable behavior include:
|
|
29
|
-
|
|
30
|
-
* The use of sexualized language or imagery, and sexual attention or
|
|
31
|
-
advances of any kind
|
|
32
|
-
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
-
* Public or private harassment
|
|
34
|
-
* Publishing others' private information, such as a physical or email
|
|
35
|
-
address, without their explicit permission
|
|
36
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
-
professional setting
|
|
38
|
-
|
|
39
|
-
## Enforcement Responsibilities
|
|
40
|
-
|
|
41
|
-
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
-
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
-
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
-
or harmful.
|
|
45
|
-
|
|
46
|
-
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
-
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
-
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
-
decisions when appropriate.
|
|
50
|
-
|
|
51
|
-
## Scope
|
|
52
|
-
|
|
53
|
-
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
-
an individual is officially representing the community in public spaces.
|
|
55
|
-
Examples of representing our community include using an official e-mail address,
|
|
56
|
-
posting via an official social media account, or acting as an appointed
|
|
57
|
-
representative at an online or offline event.
|
|
58
|
-
|
|
59
|
-
## Enforcement
|
|
60
|
-
|
|
61
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
-
reported to the community leaders responsible for enforcement through:
|
|
63
|
-
- **GitHub Issues**: Create a private issue with the `[CONDUCT]` label
|
|
64
|
-
- **GitHub Discussions**: Use the "Community" category for general concerns
|
|
65
|
-
- **Direct Contact**: Reach out to any team member through GitHub
|
|
66
|
-
- **Team Members**:
|
|
67
|
-
- [@RFS-ADRENO](https://github.com/RFS-ADRENO)
|
|
68
|
-
- [@truong9c2208](https://github.com/truong9c2208)
|
|
69
|
-
- [@JustKemForFun](https://github.com/JustKemForFun)
|
|
70
|
-
|
|
71
|
-
All complaints will be reviewed and investigated promptly and fairly by the team.
|
|
72
|
-
|
|
73
|
-
All community leaders are obligated to respect the privacy and security of the
|
|
74
|
-
reporter of any incident.
|
|
75
|
-
|
|
76
|
-
## Enforcement Guidelines
|
|
77
|
-
|
|
78
|
-
Community leaders will follow these Community Impact Guidelines in determining
|
|
79
|
-
the consequences for any action they deem in violation of this Code of Conduct:
|
|
80
|
-
|
|
81
|
-
### 1. Correction
|
|
82
|
-
|
|
83
|
-
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
84
|
-
unprofessional or unwelcome in the community.
|
|
85
|
-
|
|
86
|
-
**Consequence**: A private, written warning from community leaders, providing
|
|
87
|
-
clarity around the nature of the violation and an explanation of why the
|
|
88
|
-
behavior was inappropriate. A public apology may be requested.
|
|
89
|
-
|
|
90
|
-
### 2. Warning
|
|
91
|
-
|
|
92
|
-
**Community Impact**: A violation through a single incident or series
|
|
93
|
-
of actions.
|
|
94
|
-
|
|
95
|
-
**Consequence**: A warning with consequences for continued behavior. No
|
|
96
|
-
interaction with the people involved, including unsolicited interaction with
|
|
97
|
-
those enforcing the Code of Conduct, for a specified period of time. This
|
|
98
|
-
includes avoiding interactions in community spaces as well as external channels
|
|
99
|
-
like social media. Violating these terms may lead to a temporary or
|
|
100
|
-
permanent ban.
|
|
101
|
-
|
|
102
|
-
### 3. Temporary Ban
|
|
103
|
-
|
|
104
|
-
**Community Impact**: A serious violation of community standards, including
|
|
105
|
-
sustained inappropriate behavior.
|
|
106
|
-
|
|
107
|
-
**Consequence**: A temporary ban from any sort of interaction or public
|
|
108
|
-
communication with the community for a specified period of time. No public or
|
|
109
|
-
private interaction with the people involved, including unsolicited interaction
|
|
110
|
-
with those enforcing the Code of Conduct, is allowed during this period.
|
|
111
|
-
Violating these terms may lead to a permanent ban.
|
|
112
|
-
|
|
113
|
-
### 4. Permanent Ban
|
|
114
|
-
|
|
115
|
-
**Community Impact**: Demonstrating a pattern of violation of community
|
|
116
|
-
standards, including sustained inappropriate behavior, harassment of an
|
|
117
|
-
individual, or aggression toward or disparagement of classes of individuals.
|
|
118
|
-
|
|
119
|
-
**Consequence**: A permanent ban from any sort of public interaction within
|
|
120
|
-
the community.
|
|
121
|
-
|
|
122
|
-
## Attribution
|
|
123
|
-
|
|
124
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
125
|
-
version 2.0, available at
|
|
126
|
-
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
127
|
-
|
|
128
|
-
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
129
|
-
enforcement ladder](https://github.com/mozilla/diversity).
|
|
130
|
-
|
|
131
|
-
[homepage]: https://www.contributor-covenant.org
|
|
132
|
-
|
|
133
|
-
For answers to common questions about this code of conduct, see the FAQ at
|
|
134
|
-
https://www.contributor-covenant.org/faq. Translations are available at
|
|
135
|
-
https://www.contributor-covenant.org/translations.
|
|
136
|
-
|
|
137
|
-
## Special Considerations for zca-js
|
|
138
|
-
|
|
139
|
-
Given that zca-js is an unofficial API library for Zalo, we have additional
|
|
140
|
-
guidelines specific to our project:
|
|
141
|
-
|
|
142
|
-
### Responsible Usage
|
|
143
|
-
|
|
144
|
-
* **Respect Zalo's Terms of Service**: While this is an unofficial library,
|
|
145
|
-
users should be aware of and respect Zalo's terms of service
|
|
146
|
-
* **Account Safety**: Do not encourage or provide guidance on circumventing
|
|
147
|
-
Zalo's security measures
|
|
148
|
-
* **Privacy**: Respect user privacy and do not share personal information
|
|
149
|
-
obtained through the API
|
|
150
|
-
* **Rate Limiting**: Encourage responsible usage that doesn't overload Zalo's
|
|
151
|
-
servers
|
|
152
|
-
|
|
153
|
-
### Technical Discussions
|
|
154
|
-
|
|
155
|
-
* **Security**: When discussing security-related topics, use appropriate
|
|
156
|
-
channels and follow responsible disclosure practices
|
|
157
|
-
* **Bugs and Issues**: Report bugs and issues through proper channels
|
|
158
|
-
* **Documentation**: Help improve documentation and examples for the community
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
**Note**: This Code of Conduct is a living document and may be updated as our
|
|
163
|
-
community grows and evolves.
|