tca-js 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/api.d.ts +75 -0
- package/dist/api.js +277 -0
- package/dist/constants.d.ts +86 -0
- package/dist/constants.js +77 -0
- package/dist/context.d.ts +32 -0
- package/dist/context.js +129 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +54 -0
- package/dist/listener.d.ts +43 -0
- package/dist/listener.js +181 -0
- package/dist/lz4.d.ts +5 -0
- package/dist/lz4.js +57 -0
- package/dist/media.d.ts +27 -0
- package/dist/media.js +152 -0
- package/dist/message.d.ts +6 -0
- package/dist/message.js +113 -0
- package/dist/proto.d.ts +43 -0
- package/dist/proto.js +162 -0
- package/dist/qr-login.d.ts +41 -0
- package/dist/qr-login.js +212 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.js +2 -0
- package/package.json +26 -4
package/dist/qr-login.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loginQR = loginQR;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const ws_1 = __importDefault(require("ws"));
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
const errors_1 = require("./errors");
|
|
14
|
+
/** Nối CDP vào một tab mới (một session phẳng trên websocket của trình duyệt). */
|
|
15
|
+
async function openTab(browserWs) {
|
|
16
|
+
const ws = new ws_1.default(browserWs, { perMessageDeflate: false });
|
|
17
|
+
await new Promise((resolve, reject) => {
|
|
18
|
+
ws.once('open', () => resolve());
|
|
19
|
+
ws.once('error', reject);
|
|
20
|
+
});
|
|
21
|
+
let id = 0;
|
|
22
|
+
let sessionId;
|
|
23
|
+
const pending = new Map();
|
|
24
|
+
const handlers = [];
|
|
25
|
+
ws.on('message', (raw) => {
|
|
26
|
+
const m = JSON.parse(raw.toString());
|
|
27
|
+
if (m.id !== undefined && pending.has(m.id)) {
|
|
28
|
+
const p = pending.get(m.id);
|
|
29
|
+
pending.delete(m.id);
|
|
30
|
+
if (m.error)
|
|
31
|
+
p.reject(new Error(m.error.message));
|
|
32
|
+
else
|
|
33
|
+
p.resolve(m.result ?? {});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (m.method && (!m.sessionId || m.sessionId === sessionId))
|
|
37
|
+
for (const h of handlers)
|
|
38
|
+
h(m.method, m.params ?? {});
|
|
39
|
+
});
|
|
40
|
+
const raw = (method, params = {}, sid) => new Promise((resolve, reject) => {
|
|
41
|
+
const i = ++id;
|
|
42
|
+
pending.set(i, { resolve: resolve, reject });
|
|
43
|
+
ws.send(JSON.stringify({ id: i, method, params, ...(sid ? { sessionId: sid } : {}) }));
|
|
44
|
+
});
|
|
45
|
+
const target = await raw('Target.createTarget', { url: 'about:blank' });
|
|
46
|
+
sessionId = (await raw('Target.attachToTarget', { targetId: target.targetId, flatten: true })).sessionId;
|
|
47
|
+
return {
|
|
48
|
+
send: (method, params) => raw(method, params, sessionId),
|
|
49
|
+
on: (h) => handlers.push(h),
|
|
50
|
+
close: () => ws.close(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Chạy Chrome headless, chờ dòng "DevTools listening on ws://…". */
|
|
54
|
+
async function launchChrome(chromePath, userAgent) {
|
|
55
|
+
const dir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'tca-chrome-'));
|
|
56
|
+
const proc = (0, node_child_process_1.spawn)(chromePath, [
|
|
57
|
+
'--headless=new',
|
|
58
|
+
'--remote-debugging-port=0',
|
|
59
|
+
`--user-data-dir=${dir}`,
|
|
60
|
+
'--no-first-run',
|
|
61
|
+
'--no-default-browser-check',
|
|
62
|
+
'--disable-gpu',
|
|
63
|
+
'--disable-dev-shm-usage',
|
|
64
|
+
// Trong container chạy root — Chromium đòi tắt sandbox.
|
|
65
|
+
'--no-sandbox',
|
|
66
|
+
'--window-size=1280,900',
|
|
67
|
+
'--lang=vi-VN',
|
|
68
|
+
// UA mặc định của headless có chữ "HeadlessChrome" — thay bằng UA thường.
|
|
69
|
+
`--user-agent=${userAgent}`,
|
|
70
|
+
'about:blank',
|
|
71
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
72
|
+
const ws = await new Promise((resolve, reject) => {
|
|
73
|
+
let buf = '';
|
|
74
|
+
const timer = setTimeout(() => reject(new Error('Chrome không khởi động sau 20s')), 20_000);
|
|
75
|
+
proc.stderr.on('data', (d) => {
|
|
76
|
+
buf += d.toString();
|
|
77
|
+
const m = /DevTools listening on (ws:\/\/\S+)/.exec(buf);
|
|
78
|
+
if (m) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve(m[1]);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
proc.once('error', (e) => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
reject(e);
|
|
86
|
+
});
|
|
87
|
+
proc.once('exit', (code) => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
reject(new Error(`Chrome thoát (${code}) trước khi sẵn sàng`));
|
|
90
|
+
});
|
|
91
|
+
}).catch(async (err) => {
|
|
92
|
+
proc.kill('SIGKILL');
|
|
93
|
+
await (0, promises_1.rm)(dir, { recursive: true, force: true });
|
|
94
|
+
throw new errors_1.TikTokError('LOGIN_FAILED', `Không mở được Chrome: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
});
|
|
96
|
+
return { proc, ws, dir };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Đăng nhập TikTok bằng QR. Trả `Credentials` (cookie `.tiktok.com` + UA) — đưa vào
|
|
100
|
+
* `new TikTok().login(creds)` và LƯU LẠI để lần sau khỏi quét.
|
|
101
|
+
*/
|
|
102
|
+
async function loginQR(opts) {
|
|
103
|
+
const userAgent = opts.userAgent || constants_1.DEFAULT_USER_AGENT;
|
|
104
|
+
const maxRefresh = opts.maxRefresh ?? 3;
|
|
105
|
+
const { proc, ws, dir } = await launchChrome(opts.chromePath, userAgent);
|
|
106
|
+
let cdp = null;
|
|
107
|
+
try {
|
|
108
|
+
cdp = await openTab(ws);
|
|
109
|
+
const tab = cdp;
|
|
110
|
+
await tab.send('Network.enable');
|
|
111
|
+
await tab.send('Page.enable');
|
|
112
|
+
const result = new Promise((resolve, reject) => {
|
|
113
|
+
const urlById = new Map();
|
|
114
|
+
let refreshes = 0;
|
|
115
|
+
let scanned = false;
|
|
116
|
+
let done = false;
|
|
117
|
+
const finish = (err, creds) => {
|
|
118
|
+
if (done)
|
|
119
|
+
return;
|
|
120
|
+
done = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
opts.signal?.removeEventListener('abort', onAbort);
|
|
123
|
+
if (err)
|
|
124
|
+
reject(err);
|
|
125
|
+
else
|
|
126
|
+
resolve(creds);
|
|
127
|
+
};
|
|
128
|
+
const timer = setTimeout(() => finish(new errors_1.TikTokError('LOGIN_FAILED', 'Hết thời gian chờ quét QR')), opts.timeoutMs ?? 5 * 60_000);
|
|
129
|
+
const onAbort = () => finish(new errors_1.TikTokError('LOGIN_ABORTED', 'Đã huỷ đăng nhập QR'));
|
|
130
|
+
opts.signal?.addEventListener('abort', onAbort);
|
|
131
|
+
const body = async (requestId) => {
|
|
132
|
+
const r = await tab.send('Network.getResponseBody', { requestId }).catch(() => null);
|
|
133
|
+
if (!r)
|
|
134
|
+
return null;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(r.base64Encoded ? Buffer.from(r.body, 'base64').toString() : r.body);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/** Sau `confirmed`: trang tự chuyển hướng + đặt cookie phiên — chờ có `sessionid`. */
|
|
143
|
+
const collectCookies = async () => {
|
|
144
|
+
for (let i = 0; i < 30 && !done; i++) {
|
|
145
|
+
const { cookies } = await tab.send('Network.getAllCookies');
|
|
146
|
+
const mine = cookies.filter((c) => c.domain.endsWith('tiktok.com'));
|
|
147
|
+
if (mine.some((c) => c.name === 'sessionid' && c.value) && mine.some((c) => c.name === 'ttwid')) {
|
|
148
|
+
// Trùng tên ở `.tiktok.com` và `www.tiktok.com` → giữ bản `.tiktok.com`.
|
|
149
|
+
const byName = new Map();
|
|
150
|
+
for (const c of mine.sort((a, b) => (a.domain === '.tiktok.com' ? 1 : 0) - (b.domain === '.tiktok.com' ? 1 : 0))) {
|
|
151
|
+
byName.set(c.name, { name: c.name, value: c.value });
|
|
152
|
+
}
|
|
153
|
+
return finish(null, { cookies: [...byName.values()], userAgent });
|
|
154
|
+
}
|
|
155
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
156
|
+
}
|
|
157
|
+
finish(new errors_1.TikTokError('LOGIN_FAILED', 'Đã xác nhận nhưng không thấy cookie sessionid'));
|
|
158
|
+
};
|
|
159
|
+
tab.on((method, p) => {
|
|
160
|
+
if (done)
|
|
161
|
+
return;
|
|
162
|
+
if (method === 'Network.responseReceived') {
|
|
163
|
+
const res = p.response;
|
|
164
|
+
urlById.set(p.requestId, res.url);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (method !== 'Network.loadingFinished')
|
|
168
|
+
return;
|
|
169
|
+
const url = urlById.get(p.requestId) ?? '';
|
|
170
|
+
if (url.includes('/passport/web/get_qrcode/')) {
|
|
171
|
+
void body(p.requestId).then((j) => {
|
|
172
|
+
const qr = j?.data?.qrcode;
|
|
173
|
+
if (typeof qr === 'string' && qr) {
|
|
174
|
+
opts.onEvent?.({
|
|
175
|
+
type: 'qr',
|
|
176
|
+
image: `data:image/png;base64,${qr}`,
|
|
177
|
+
expiresAt: Number(j?.data?.expire_time ?? 0) * 1000 || Date.now() + 60_000,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else if (url.includes('/passport/web/check_qrconnect/')) {
|
|
183
|
+
// Trang hỏi song song 3 vùng (sg/us/eu) — chỉ vùng của tài khoản đổi trạng thái, còn lại vẫn "new".
|
|
184
|
+
void body(p.requestId).then((j) => {
|
|
185
|
+
const status = String(j?.data?.status ?? '');
|
|
186
|
+
if (status === 'scanned' && !scanned) {
|
|
187
|
+
scanned = true;
|
|
188
|
+
opts.onEvent?.({ type: 'scanned' });
|
|
189
|
+
}
|
|
190
|
+
else if (status === 'confirmed') {
|
|
191
|
+
opts.onEvent?.({ type: 'confirmed' });
|
|
192
|
+
void collectCookies();
|
|
193
|
+
}
|
|
194
|
+
else if (status === 'expired' && !scanned) {
|
|
195
|
+
if (refreshes++ >= maxRefresh)
|
|
196
|
+
finish(new errors_1.TikTokError('LOGIN_FAILED', 'QR hết hạn, không ai quét'));
|
|
197
|
+
else
|
|
198
|
+
void tab.send('Page.reload');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
await tab.send('Page.navigate', { url: `${constants_1.WEB}/login/qrcode` });
|
|
205
|
+
return await result;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
cdp?.close();
|
|
209
|
+
proc.kill('SIGKILL');
|
|
210
|
+
await (0, promises_1.rm)(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Một cookie — đúng dạng `chrome.cookies.getAll` / CDP `Storage.getCookies` trả về (thừa trường không sao). */
|
|
2
|
+
export interface CookieInput {
|
|
3
|
+
name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
/** Thứ cần để đăng nhập lại — lưu lại sau mỗi lần đăng nhập, như `{cookie, imei, userAgent}` của zca-js. */
|
|
7
|
+
export interface Credentials {
|
|
8
|
+
/** Cookie của `.tiktok.com`: mảng `{name, value}`, object `{name: value}`, hoặc chuỗi `a=b; c=d`. */
|
|
9
|
+
cookies: CookieInput[] | Record<string, string> | string;
|
|
10
|
+
/** Mã máy IM (`wid`) — bỏ trống thì `login` hỏi TikTok theo `ttwid`. Chỉ dùng khi không hỏi được. */
|
|
11
|
+
deviceId?: string;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TikTokOptions {
|
|
15
|
+
/** Ghi log gỡ lỗi — mặc định im lặng. */
|
|
16
|
+
logger?: (level: 'debug' | 'warn', msg: string) => void;
|
|
17
|
+
/** Hạn chờ mỗi request HTTP (ms). */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Đủ để nhắm một hội thoại: gửi tin, đọc tin, đánh dấu đã đọc. Mọi `TikTokMessage` đều thoả. */
|
|
21
|
+
export interface ConversationRef {
|
|
22
|
+
conversationId: string;
|
|
23
|
+
conversationShortId: string;
|
|
24
|
+
}
|
|
25
|
+
export interface TikTokImage {
|
|
26
|
+
/** Mã ảnh trên kho TikTok (đã mã hoá). */
|
|
27
|
+
uri: string;
|
|
28
|
+
/** Link tải bản gốc — cần cookie, nội dung là AES-256-GCM. */
|
|
29
|
+
url: string;
|
|
30
|
+
thumbUrl: string;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
/** Khoá AES-256-GCM (hex) để giải `url` — `api.downloadImage` lo việc này. */
|
|
34
|
+
decryptKey: string;
|
|
35
|
+
}
|
|
36
|
+
/** Sticker (nhãn dán) — kể cả sticker khách tự tạo (`stickerType` 15). Link CDN công khai, có chữ ký + hạn vài ngày. */
|
|
37
|
+
export interface TikTokSticker {
|
|
38
|
+
stickerId: string;
|
|
39
|
+
/** 14 = sticker có sẵn của TikTok, 15 = sticker người dùng tự tạo (ảnh chế, AI…). */
|
|
40
|
+
stickerType: number;
|
|
41
|
+
url: string;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}
|
|
45
|
+
/** Một reaction trên tin: emoji + những người đã thả (uid). `e:love` của app điện thoại đã gộp vào ❤️. */
|
|
46
|
+
export interface TikTokReaction {
|
|
47
|
+
emoji: string;
|
|
48
|
+
userIds: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface TikTokMessage extends ConversationRef {
|
|
51
|
+
serverMessageId: string;
|
|
52
|
+
/** Vị trí trong hội thoại (µs) — làm mốc đọc tiếp tin cũ hơn. */
|
|
53
|
+
index: string;
|
|
54
|
+
/** `MessageType` — 7 chữ, 1802 ảnh, 1805 sticker, 8 chia sẻ video, 1 hệ thống. */
|
|
55
|
+
type: number;
|
|
56
|
+
/** `command` = lệnh ẩn (đã đọc…) — listener phát qua sự kiện `command`, không qua `message`. */
|
|
57
|
+
kind: 'text' | 'image' | 'sticker' | 'video' | 'system' | 'command' | 'other';
|
|
58
|
+
senderId: string;
|
|
59
|
+
isSelf: boolean;
|
|
60
|
+
/** Nội dung chữ (`kind = text`), rỗng với loại khác. */
|
|
61
|
+
text: string;
|
|
62
|
+
image: TikTokImage | null;
|
|
63
|
+
sticker: TikTokSticker | null;
|
|
64
|
+
reactions: TikTokReaction[];
|
|
65
|
+
/** JSON thô của nội dung (`{"aweType":0,"text":"…"}`…). */
|
|
66
|
+
content: unknown;
|
|
67
|
+
/** Giờ gửi, ms epoch. */
|
|
68
|
+
createdAt: number;
|
|
69
|
+
clientMessageId: string;
|
|
70
|
+
ext: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
export interface TikTokConversation extends ConversationRef {
|
|
73
|
+
participants: string[];
|
|
74
|
+
/** Người còn lại trong hội thoại 1-1. */
|
|
75
|
+
peerId: string;
|
|
76
|
+
}
|
|
77
|
+
export interface TikTokUser {
|
|
78
|
+
uid: string;
|
|
79
|
+
/** `@username` (không có @). */
|
|
80
|
+
uniqueId: string;
|
|
81
|
+
nickname: string;
|
|
82
|
+
/** Ảnh đại diện tốt nhất (ưu tiên jpeg/webp/png, tránh heic trình duyệt không đọc). */
|
|
83
|
+
avatar: string;
|
|
84
|
+
/** Mọi link ảnh đại diện TikTok trả, xếp theo ưu tiên — link có chữ ký, có hạn; `downloadAvatar` thử lần lượt. */
|
|
85
|
+
avatars: string[];
|
|
86
|
+
}
|
|
87
|
+
export interface TikTokAccount {
|
|
88
|
+
uid: string;
|
|
89
|
+
username: string;
|
|
90
|
+
screenName: string;
|
|
91
|
+
avatar: string;
|
|
92
|
+
}
|
|
93
|
+
/** File đính kèm — giống `attachments` của zca-js. */
|
|
94
|
+
export interface ImageInput {
|
|
95
|
+
data: Buffer;
|
|
96
|
+
/** Chỉ cần đuôi để TikTok biết định dạng: `anh.jpg`, `bill.png`. */
|
|
97
|
+
filename: string;
|
|
98
|
+
}
|
|
99
|
+
export interface MessagePage {
|
|
100
|
+
messages: TikTokMessage[];
|
|
101
|
+
hasMore: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface ConversationPage {
|
|
104
|
+
conversations: TikTokConversation[];
|
|
105
|
+
/** Tin mới nhất kèm theo — tiện dựng danh sách hộp thư. */
|
|
106
|
+
messages: TikTokMessage[];
|
|
107
|
+
cursor: string;
|
|
108
|
+
hasMore: boolean;
|
|
109
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tca-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Unofficial TikTok Chat API for JavaScript — read/send TikTok direct messages (text, images, reactions) and listen over websocket using session cookies, no browser needed.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "rice2411",
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/rice2411/tca-js.git" },
|
|
8
|
+
"homepage": "https://github.com/rice2411/tca-js#readme",
|
|
9
|
+
"bugs": { "url": "https://github.com/rice2411/tca-js/issues" },
|
|
10
|
+
"keywords": ["tiktok", "tiktok-api", "tiktok-dm", "tiktok-chat", "direct-message", "unofficial", "chat", "websocket", "zca-js"],
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": ["dist"],
|
|
14
|
+
"engines": { "node": ">=22" },
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"smoke": "node scripts/smoke.mjs"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"ws": "^8.18.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"@types/ws": "^8.5.11",
|
|
26
|
+
"typescript": "5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|